使用 Hooks 實現一個簡單的狀態管理器

stockholm發表於2019-04-08

“狀態管理”是 React 中繞不開的一個話題。因為 React 中資料是自上(祖先元件)而下流動的,當層級較深的元件需要訪問祖先元件的狀態時,通常需要把該狀態通過多個元件傳遞下去。元件必須要傳遞該元件實際上並不使用的狀態,導致元件之間耦合嚴重,有悖於元件的設計原則。這時候,我們就需要“狀態管理器”來提供一種不需要多層元件傳遞也可以訪問的全域性狀態。

目前最流行的解決方案應該是 Redux 了。Redux 是一個嚴格的單向資料流、單一資料來源的狀態管理器。因為 Redux 對“何時”還有“如何”修改狀態做出了嚴格的限制,使得狀態的變化具有可預測性且可以記錄。

通常我們並不會直接使用 Redux 的 API,一般是使用像 React-Redux 這樣的庫。

為了獲取 state 和 dipatch,需要使用 React-Redux 提供的 connect 函式,通過高階元件的形式注入給相應的元件。這種寫法十分繁瑣,會在元件樹裡增加不必要的元件巢狀。使用 connect 函式分離了元件的檢視層和邏輯層,這也是不再被推薦的寫法。

雖然 React-Redux 寫法繁瑣,但 Redux 在社群中依然有很多使用者,主要還是因為 Redux 的設計思想受到了多數人的推崇。

React v16.8 以後,推出了 Hooks。它允許我們通過 useReducer 使用 Redux 的核心思想,而不需要引入 Redux 或者使用其他 Redux 限制我們使用的寫法。結合 Context,我們可以建立自定義的狀態管理器。

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);

useReducer 是 React 自帶的 Hooks,它接收兩個引數,reducer 和 state 的初始值。呼叫後會返回一個陣列,陣列第一項是 state,第二項是 dispatch 方法。

當元件中有比較複雜的狀態需要管理,或者需要跨越很深的元件層級去更新狀態的時候,useReducer 是比 useState 更好的選擇。

下面是一個 useReducer 的使用例項:

const initialState = { count: 0 };

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({ initialState }) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </>
  );
}
複製程式碼

useReducer 的使用十分簡潔,卻涵蓋了 Redux 的核心部分,通過 dispatch 方法派發 action 物件,reducer 根據 action 返回一個新的 state。

不過如果只是單獨使用 useReducer 的話,依然要跨越多個元件層級傳遞 dispatch 方法。幸好,我們還有 Context。

Context

Context 提供了一種可以傳遞資料給整個元件樹而不用一層一層向下傳遞的方法。

呼叫 React.createContext 方法生成一個 Context 物件,這個 Context 物件包含了兩個屬性 ProviderConsumer,這兩個屬性都是 React 元件。

Provider 接收一個屬性 value ,該屬性會被 Provider 後代元件中的 Consumer 接收到。

<MyContext.Provider value={/* 這裡放一些值 */}>
複製程式碼

Consumer 使用 render prop 為其子元件提供 value 屬性,該屬性早已在 Provider 中定義好了。

<MyContext.Consumer>
  {value => /* 根據 context value 渲染一些內容 */}
</MyContext.Consumer>
複製程式碼

store

瞭解了 useReducer 和 Context 之後,我們就可以把這兩者結合起來,實現一個簡單的狀態管理器。

首先要宣告一個 Context 物件,用來承載我們的狀態管理器。

StoreContext.js

import { createContext } from 'react';

const StoreContext = createContext();

export default StoreContext;
複製程式碼

為了讓元件樹都可以訪問到共享的狀態,我們還需要實現一個 Provider 元件。

Provider.js

import React, { useReducer } from 'react';
import PropTypes from 'prop-types';
import StoreContext from './StoreContext';

export default function Provider({ children, reducer, initialState }) {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <StoreContext.Provider value={[state, dispatch]}>
      {children}
    </StoreContext.Provider>
  );
}

Provider.propTypes = {
  children: PropTypes.element.isRequired,
  reducer: PropTypes.func.isRequired,
  initialState: PropTypes.any.isRequired,
};
複製程式碼

Provider 接收兩個 props,reducer 和 initialState。把這兩個屬性作為引數,呼叫 useReducer 來獲取到 state 和 dispatch。然後再把 state 和 dispatch 原封不動的交給 StoreContext.Provider,這樣我們就可以在 Provider 的後代元件中獲取到 state 和 dispatch 了。

在 React-Redux 中一般是使用 connect 函式,通過高階元件的形式,把 state 和 dispatch 注入給相應的元件。但是現在我們有了 Hooks,就可以使用一種更簡單的寫法來獲取 state 和 dispatch。

useStore.js

import { useContext } from 'react';
import StoreContext from './StoreContext';

export default function useStore() {
  const [state, dispatch] = useContext(StoreContext);
  return { state, dispatch };
}
複製程式碼

這裡引入之前宣告的 StoreContext,並呼叫 useContext 方法,以獲取 useContext.Provider 中儲存的 state 和 dispatch。這裡通過把 state 和 dispatch 以一個物件的形式返回出去,主要是為了方便 IDE 智慧提示。

注意這裡函式的命名,要以“use”作為開頭,以便 React 能識別這是一個自定義 Hook,並做相應的處理。

我們把上面三個檔案儲存在 store 資料夾下,並建立一個 index.js 檔案用來匯出 ProvideruseStore

所有程式碼都可以在這裡線上檢視。

具體使用時,我們先宣告 reducer 和初始 state,就跟使用 Redux 一樣。然後從 store 資料夾中引入 Provider,並將 reducer 和初始 state 作為 props 傳遞給 Provider

import React from 'react';
import { Provider } from './store';
import Count from './Count';

function reducer(state, action) {
  switch (action.type) {
    case 'increase': {
      return state + 1;
    }

    case 'decrease': {
      return state - 1;
    }

    default: {
      return state;
    }
  }
}

const initialState = 0;

function App() {
  return (
    <Provider reducer={reducer} initialState={initialState}>
      <Count />
    </Provider>
  );
}

export default App;
複製程式碼

接下來,我們只需要在元件中通過一行程式碼,就可以獲取到 state 和 dispatch:

const { state, dispatch } = useStore();
複製程式碼

和 Redux 只有單一的狀態樹不同,我們可以在元件樹中多次插入 Provider。呼叫 useStore 時會向上查詢,並自動獲取離當前元件最近的 Provider 中儲存的狀態。

假如你只是希望能夠使用一個簡單的狀態管理器,不希望像使用 Redux 那樣去管理一個複雜的單一狀態樹,useReducer + Context 是最合適的選擇。不過這樣一來也無法使用 Redux 中介軟體及其相關生態,雖然 Hooks 允許我們靈活的編寫和複用程式碼,但具體的實現還是需要根據每個專案去做決定。

相關文章