“状态管理”是 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 对象包含了两个属性 Provider
和 Consumer
,这两个属性都是 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
文件用来导出 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 允许我们灵活的编写和复用代码,但具体的实现还是需要根据每个项目去做决定。