「狀態管理」是 React 中繞不開的一個話題。由於 React 中數據是自上(祖先組件)而下流動的,當層級較深的組件須要訪問祖先組件的狀態時,一般須要把該狀態經過多個組件傳遞下去。組件必需要傳遞該組件實際上並不使用的狀態,致使組件之間耦合嚴重,有悖於組件的設計原則。這時候,咱們就須要「狀態管理器」來提供一種不須要多層組件傳遞也能夠訪問的全局狀態。javascript
目前最流行的解決方案應該是 Redux 了。Redux 是一個嚴格的單向數據流、單一數據源的狀態管理器。由於 Redux 對「什麼時候」還有「如何」修改狀態作出了嚴格的限制,使得狀態的變化具備可預測性且能夠記錄。html
一般咱們並不會直接使用 Redux 的 API,通常是使用像 React-Redux 這樣的庫。java
爲了獲取 state 和 dipatch,須要使用 React-Redux 提供的 connect
函數,經過高階組件的形式注入給相應的組件。這種寫法十分繁瑣,會在組件樹裏增長沒必要要的組件嵌套。使用 connect
函數分離了組件的視圖層和邏輯層,這也是再也不被推薦的寫法。react
雖然 React-Redux 寫法繁瑣,但 Redux 在社區中依然有不少使用者,主要仍是由於 Redux 的設計思想受到了多數人的推崇。redux
React v16.8 之後,推出了 Hooks。它容許咱們經過 useReducer
使用 Redux 的核心思想,而不須要引入 Redux 或者使用其餘 Redux 限制咱們使用的寫法。結合 Context,咱們能夠建立自定義的狀態管理器。數組
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
ide
useReducer 是 React 自帶的 Hooks,它接收兩個參數,reducer 和 state 的初始值。調用後會返回一個數組,數組第一項是 state,第二項是 dispatch 方法。函數
當組件中有比較複雜的狀態須要管理,或者須要跨越很深的組件層級去更新狀態的時候,useReducer
是比 useState
更好的選擇。ui
下面是一個 useReducer 的使用實例:spa
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 提供了一種能夠傳遞數據給整個組件樹而不用一層一層向下傳遞的方法。
調用 React.createContext
方法生成一個 Context 對象,這個 Context 對象包含了兩個屬性 Provider
和 Consumer
,這兩個屬性都是 React 組件。
Provider
接收一個屬性 value ,該屬性會被 Provider
後代組件中的 Consumer
接收到。
<MyContext.Provider value={/* 這裏放一些值 */}>
複製代碼
Consumer
使用 render prop 爲其子組件提供 value 屬性,該屬性早已在 Provider
中定義好了。
<MyContext.Consumer>
{value => /* 根據 context value 渲染一些內容 */}
</MyContext.Consumer>
複製代碼
瞭解了 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
文件用來導出 Provider
和 useStore
。
全部代碼均可以在這裏在線查看。
具體使用時,咱們先聲明 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 容許咱們靈活的編寫和複用代碼,但具體的實現仍是須要根據每一個項目去作決定。