React Hook 實戰指南!(3)

實現React Hook版的Store

前言

衆所周知,由於React自己只是View層的框架,對於總體業務架構來講是有缺失的,因此咱們常常會在React應用中接入Flux、Redux等架構模式,固然也能夠選擇使用Mobx(相似Vuex)等集成工具。javascript

就拿使用較廣的Redux架構來講,在React中實現後,每每須要將store中的數據掛載到組件的狀態中,當subscribe到state改變後再調用setState來更新組件對應的狀態來實現數據同步。好的,問題來了,使用Hook後組件內部該怎麼處理呢?其實很簡單,只須要利用useState來構建狀態就行了。vue

const Example = () => {
  const [count, setCount] = useState(store.getState().count)
  store.subscribe(() => {
  	setCount(store.getState().count)
  })
	return (<div>{count}</div>)
}
複製代碼

是否是很簡單呢?固然了,咱們通常會使用react-redux來簡化redux的使用,使用來Hook後,對應的工具庫固然也要作更換,增長一大波學習成本。java


useReducer的使用

咱們要實現的Todolist實例組件間其實有很強的聯動性,因此必然要將一些數據進行集中的管理。react

其實React 提供了不少Hook工具,不僅有咱們看到的useState,其餘的咱們慢慢來學習,咱們先來學習一下useReducer的使用,這個東西就能夠幫助咱們構建一個簡版的Store,雖說是簡陋來一些,可是咱們構建的合理一些其實也能知足應用的需求。vuex

const [state, dispatch] = useReducer(reducer, initialArg, init);
複製代碼

useReducer實際上是useState的升級版本,能夠利用一個(state, action) => newState 的 reducer來構建數據,而且還能夠生成與之匹配的dispatch方法,你們看到這些就忽然發現,這個玩意和redux很像對吧,哈哈,ok,那咱們再來看一下這個東西怎麼用。redux

// 初始狀態
const initialState = {count: 0};

// reducer純函數,接收當前的state和action,action身上帶有標示性的type
// reducer每次執行都會返回一個新的數據
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  // 利用useReducer生成狀態
  // 第一個參數爲處理數據的reducer,第二個參數爲初始狀態
  // dispatch每次調用都會觸發reducer來生成新的狀態,具體的處理由傳入的action決定
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <> Count: {state.count} <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ); } 複製代碼

useReducer能夠接收三個參數。第一個參數爲處理數據的reducer,第二個參數爲初始狀態,第三個參數能夠不傳,它的用處是惰性處理初始狀態,例如:數組

// 初始狀態
const initialState = 0
function reducer(state, action) {
  // ...
}
function init (state) {
 	return { count: state } 
}
function Counter() {
  // 雖然咱們傳入的初始狀態是 0 ,可是最終的初始化狀態爲: init(initialState)
  // 因此最終初始狀態爲{ count: 0 }
  // 其實用處不大...脫褲子放屁的感受....
  const [state, dispatch] = useReducer(reducer, initialState, init);
  return (
    // ...
  );
}
複製代碼

ok,這不state也有了,reducer也有了,根據dispatch我們再整個actionCreator 出來,小小的Redux架構不就搭建完成了麼?架構

在動手以前,咱們想到一個問題,useReducer是要在函數組件裏面使用的呀,咱們不能在每個要使用state的組件中都利用useReducer構建一個store吧?那樣不就各自爲政了嘛,還談什麼狀態集中管理,那怎麼辦呢?框架

答案就是:整出一個你們的爸爸(最外層的父組件)來,而後套在外面,由這個父組件來管理這些狀態,而後再將狀態和action方法給內部的子組件傳入。ide

可是,這樣依然存在一個問題待解決:一層一層的傳數據太麻煩了,不科學。那該怎麼解決呢?

沒錯,聰明的小夥伴已經想到了,咱們用context來解決數據的傳遞問題,那麼利用context傳遞有什麼很差的地方嗎?

答案是沒啥事兒,react-redux是怎麼讓全部的容器組件都能獲取到store中的狀態再傳遞給UI組件的呢,還不是在最外面有個Provider利用context樹給它們提供了store嘛!

ok,一切問題都解決了,準備開始!


實現Store

構建store的方法其實很簡單,可是爲告終構分離順便再多搞一個知識點,咱們準備利用一個自定義Hook來完成store的構建。

自定義Hook構建store

自定義Hook的目的是將一些邏輯提高到某個可重用的函數中,相似於HOC的存在同樣,自定義的Hook須要有這樣的條件:

  1. 首先,它得是個函數
  2. 其次,它得返回點東西給組件們使用

OK,內心有數了,咱們先把其餘的東西構建出來(actions, initialState):

默認狀態,我們利用immutable來構建不可變狀態,優化性能:

import { is, fromJS } from 'immutable'
// fromJS能夠將一個普通結構的數據生成爲immutable結構的數據
const initialState = fromJS({
  items: []
})
複製代碼

reducer,在內部已經實現了關於Todolist業務的一些處理,咱們準備將todolist數據存放在localStorage中,爲了操做方便使用localstorage包:

import LocalStorage from 'localstorage'
// 這個東東能夠方便的操做localStorage
const TodoList_LS = new LocalStorage('todolist_')
// reducer接受當前的狀態(設置默認狀態)以及action
// action中包含這次動做的type標示與payload載體
const reducer = (state = initialState, { type, payload }) => {
  // 準備返回的狀態
  let result = state
  switch (type) {
      // 更新所有items
    case 'UPDATE_ITEMS':
      // immutable基本操做,設置items後返回的就是一個新的狀態
      // 此時result !== state 喲
      result = state.set('items', fromJS(payload.items))
      break
    	// 新建某個item
    case 'CREATE_ITEM':
      result = state.set('items', state.get('items').push(fromJS(payload.item)))
      break
      // 完成某個item
    case 'FINISH_ITEM':
      result = state.set('items', state.get('items').update(
        state.get('items').findIndex(function(item) { 
          return is(item.get('id'), payload.id)
        }), function(item) {
          return item.set('finished', !item.get('finished'))
        })
      )
      break
      // 更新item的title和description
    case 'UPDATE_ITEM':
      result = state.set('items', state.get('items').update(
        state.get('items').findIndex(function(item) { 
          return is(item.get('id'), payload.item.id)
        }), function(item) {
          item = item.set('title',  payload.item.title)
          item = item.set('description',  payload.item.description)
          return item
        })
      )
      break
      // 刪除某個item
    case 'DELETE_ITEM':
      let list = state.get('items')
      let index = list.findIndex((item) => is(item.get('id'), payload.id))
      result = state.set('items', list.remove(index))
      break
    default: break
  }
  // 將更新後的items存入localStorage中
  TodoList_LS.put('items', result.get('items').toJS())
  return result
}
複製代碼

在這裏簡單說一下immutable的使用,當數據轉換爲immutable數據後,利用對應的set、get、update等APi操做數據後都能返回一個新的數據。

你們能夠看到reducer的操做基本與redux的reducer構建方式同樣,內部包含的也僅僅是一些增刪改差的簡單操做。

接下來咱們再來創造一個actions工具,內含不少方法,每一個方法均可以調用dispatch來觸發reducer的執行並傳入對應的action(包含標識的type和數據載體payload)。

const actions = {
  getInitialItems () {
    let [err, items] = TodoList_LS.get('items')
    if (err) items = []
    this.dispatch({
      type: 'UPDATE_ITEMS',
      payload: { items }
    })
  },
  createTodoItem ({ item }) {
    let [err, id] = TodoList_LS.get('id')
    if (err) id = 0
    item.id = ++id
    item.finished = false
    this.dispatch({
      type: 'CREATE_ITEM',
      payload: { item }
    })
    TodoList_LS.put('id', item.id)
  },
  finishTodo ({ id }) {
    this.dispatch({
      type: 'FINISH_ITEM',
      payload: { id }
    })
  },
  deleteTodo ({ id }) {
    this.dispatch({
      type: 'DELETE_ITEM',
      payload: { id }
    })
  },
  updateTodoItem ({ item }) {
    this.dispatch({
      type: 'UPDATE_ITEM',
      payload: { item }
    })
  } 
}

複製代碼

你們能夠看到在actions的方法中都在調用一個this.dispatch方法,這個方法是哪來的呢,咱們一下子就把useReducer生成出來的reducer掛載到actions身上不就有了麼。

最後輪到咱們的自定義Hook了,甩出來瞅瞅:

// 構建Store的Custom Hook
const StoreHook = ( ) => {
  // 利用useReducer構建state與dispatch
  let [ state, dispatch ] = useReducer(reducer, initialState)
  
  // 爲actions掛載dispatch,防止更新的時候掛載屢次
  if (!actions.dispatch) actions.dispatch = dispatch
  
  // immutable數據轉換爲普通結構數據
  let _state = state.toJS()
  
  // Hook生成的數據
  let result = [
    _state, 
    actions
  ]

  return result
}
複製代碼

咱們構建的自定義Hook-StoreHook在實例中沒有複用的場景,在這裏僅僅是爲了分離Store的構建以及學習自定義Hook。

在StoreHook中利用useReucer生成了state和dispatch方法,將dispatch方法掛載在actions身上以便actions內部的方法來調用觸發reducer,將生成的狀態及actions返回出去,這樣使用StoreHook的組件就能夠獲得咱們構建好準備集中管理的state和actions了。

let [state, actions] = StoreHook()
複製代碼

利用Context及Custom Hook來使用state & actions

上面咱們以及討論過了,只要使用咱們自定義的StoreHook,就能夠獲得state和actions,可是整個實例只能集中的管理一個state,因此咱們不能在多個組件中同時使用StoreHook,因此咱們須要構建一個專門用來構建state和actions並將其傳遞給全部子組件的「你們的爸爸」組件。

export const StoreContext = React.createContext({})
export const HookStoreProvider = (props) => {
  let [state, actions] = StoreHook()
  return (
    <StoreContext.Provider value = {{ state, actions }}> { props.children } </StoreContext.Provider> ) } 複製代碼

你們能夠看到HookStoreProvider組件在構建了context將state和actions進行傳遞,很是棒,把它包在組件結構的最外面吧。

import { HookStoreProvider } from '@/hooks/todolist'
class App extends Component {
  render () {
    return (
      <HookStoreProvider> <TodoList/> </HookStoreProvider>
    )
  }
}
export default App
複製代碼

ok,那麼咱們的組件須要怎麼去使用HookStoreProvider在context樹上傳遞的狀態和組件呢?傳統的context使用方法倒也能夠:

import { StoreContext } from './store.js'
const TodoListContent = () => {
	return (
    <StoreContext.Consumer> { (value) => (<div>{ value }</div>) } </StoreContext.Consumer> ) } 複製代碼

這樣的使用方式有點麻煩,幸虧React Hook提供來useContext的Hook工具,這就簡單多了:

let values = useContext(StoreContext)
let { state, actions } = values
複製代碼

useContext傳入Context對象後能夠返回此Context對象掛載在context樹上的數據,這就不須要什麼Consumer了,簡單粗暴,那麼咱們在大膽一點,爲了讓想要使用狀態和actions的組件連useContext都不用寫,並且還能夠經過傳個getter參數就能去到state中的某個狀態的衍生狀態,(相似於vuex中的getters),這樣的「中間商」正好能夠再利用Custom Hook(自定義Hook)來構建:

// 關於state的衍生狀態
const getters = {
  // 關於todos的展現信息數據
  todoDetail: (state) => {
    let items = state.items
    let todoFinishedItems = items.filter(item => item.finished)
    let todoUnfinishedItems = items.filter(item => !item.finished)
    let description = `當前共有 ${items.length} 條待辦事項,其中${todoFinishedItems.length}條已完成,${todoUnfinishedItems.length}條待完成。`
    // 返回描述、已完成、未完成、所有的items
    return {
      description,
      todoItems: items,
      todoFinishedItems,
      todoUnfinishedItems
    }
  },
  // 返回全部的items
  items: (state) => {
    return state.items
  }
}
// 自定義Hook,接受context中的狀態並根據傳入的getter來獲取對應的getter衍生數據
export const useTodolistStoreContext = ( getter ) => {
  let {state, actions} = useContext(StoreContext)

  let result = [
    getter ? getters[getter](state) : state, 
    actions
  ]
  return result
}
複製代碼

上面的代碼構建了getters工具和useTodolistStoreContext自定義Hook,這樣只要在組件內使用這個Hook就能夠獲得state或者衍生的getters以及actions了。

const TodoListContent = (props) => {
  let [items] = useTodolistStoreContext('items')
  return (
    <div className="todolist-content">{ items }</div>
  )
}

複製代碼

後語

這樣咱們的關於Store的構建也以及完成了,在這裏咱們研究了useReducer、useContext以及自定義Hook的使用了。後面的內容中,咱們將在實例的繼續構建中學習useState,useEffect,useRef的使用。

相關文章
相關標籤/搜索