什么是状态?状态就是 UI 中的动态数据。

React 状态管理

React的状态管理主要分三类:Local state、Context、第三方库。
React 状态管理 - 图1

Local State

local state 的管理就是 useState 和 useReducer 的天下了。

useState - 更细粒度的状态

类组件:将所有的state都放在一个对象中
useState:将组件内的状态再拆分,以更细的粒度维护

  1. import {useCallback, useState} from 'react';
  2. const Foo = () => {
  3. const [stateA, setStateA] = useState(0);
  4. const [stateB, setStateB] = useState(0);
  5. const handleAdd = useCallback(
  6. () => { setStateA(prev => prev + 1); },
  7. []
  8. );
  9. const handleSubtract = useCallback(
  10. () => { setStateB(prev => prev - 1); },
  11. []
  12. );
  13. return (
  14. <>
  15. <Button onClick={handleAdd}>{stateA}</Button>
  16. <Button onClick={handleSubtract}>{stateB}</Button>
  17. </>
  18. );
  19. };

useReducer - 复杂逻辑抽象和复用

使用useReducer可以认为是在组件内生成一个独立的redux store,并且这个reducer的逻辑可以在不同的组件中复用

当state的计算逻辑比较复杂,或者派生状态的变化存在共性,或者reducer逻辑可以复用时,可以优先考虑用useReducer。

例如常见的列表分页逻辑封装:

  1. import {useReducer} from 'react';
  2. const initialState = { pageNum: 1, pageSize: 15 };
  3. const reducer = (state, action) => {
  4. switch (action.type) {
  5. case 'next': // 下一页
  6. return { ...state, pageNum: state.pageNum + 1 };
  7. case 'prev': // 前一页
  8. return { ...state, pageNum: state.pageNum - 1 };
  9. case 'changePage': // 跳转到某页
  10. return { ...state, pageNum: action.payload };
  11. case 'changeSize': // 更改每页展示条目数
  12. return { pageNum: 1, pageSize: action.payload };
  13. default:
  14. return state;
  15. }
  16. };
  17. const Page = () => {
  18. const [pager, dispatch] = useReducer(reducer, initialState);
  19. return (
  20. <Table
  21. pageNum={pager.pageNum}
  22. pageSize={pager.pageSize}
  23. onGoNext={() => dispatch({ type: 'next' })}
  24. onGoPrev={() => dispatch({ type: 'prev' })}
  25. onPageNumChange={(num) => dispatch({ type: 'changePage', payload: num })}
  26. onPageSizeChange={(size) => dispatch({ type: 'changeSize', payload: size })}
  27. />
  28. );
  29. };

使用 useReducer 还有一个优点是可以优化深层 子组件 需要触发更新时的应用性能。

假设我们在父组件定义了一个state,在子组件中有要更改父组件state的需求,以往惯用的做法是在父组件定义相关的callback,然后一层层地透传给子组件。

组件层级特别深 callback特别多 的时候,就会回想起被prop透传支配的恐惧,写子组件prop的类型也要脱半层皮。
并且使用useCallback封装的方法有可能因为依赖的变量更新返回新的引用,而导致透传途径的子组件都可能触发更新。

使用useReducer的话,可以结合useContext,只把dispatch传到子组件中。并且dispatch生成之后引用恒定不变,不会触发context可能的force update。

  1. import {createContext, useReducer, useContext} from 'react';
  2. const ParentDispatch = createContext(null);
  3. const Parent = () => {
  4. const [state, dispatch] = useReducer(reducer, initialState);
  5. return (
  6. <ParentDispatch.Provider value={dispatch}>
  7. <DeepTree parentState={state} />
  8. </ParentDispatch.Provider>
  9. );
  10. };
  11. // 深层子组件
  12. const DeepChild = () => {
  13. const dispatch = useContext(ParentDispatch);
  14. const handleClick = () => {
  15. dispatch({ type: 'add', payload: 'hello' });
  16. };
  17. return <button onClick={handleClick}>Add</button>;
  18. };

Context

在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,context提供了可以跨组件层级传递prop的API。

useContext

FC中结合createContext和useContext使用,可见上文useReducer中的例子。

Context的问题

在react里,context是个反模式的东西,不同于redux等的细粒度响应式更新,context的值一旦变化,所有依赖该context的组件全部都会force update,因为context API并不能细粒度地分析某个组件依赖了context里的哪个属性,并且它可以穿透React.memo和shouldComponentUpdate的对比,把所有涉事组件强制刷新。

Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。

综上,在系统中跟业务相关、会频繁变动的数据在共享时,应谨慎使用context。

如果决定使用context,可以在一些场景中,将多个子组件 依赖的不同context属性 提升到一个父组件中,由父组件 订阅context 并以prop的方式下发,这样可以使用子组件的memo、shouldComponentUpdate生效。
React 状态管理 - 图2

状态管理库

如今最火的React状态管理库莫过于Redux、Mobx、Recoil,其中Redux和Mobx都是老牌强手代表,Recoil则是这两年最火的后起之秀。

Redux的设计是为以下原则服务的:要让状态的变化可追踪,可重复,可维护,因此才会有 reducer, action, middleware 这些概念。

Redux

React 状态管理 - 图3
React-redux推出useDispatch、useSelector等hook之后,减少了大量以前使用高阶组件(container)来连接(connect)store和view的代码,极大降低了获取state封装action的成本,用法也更加灵活。

Redux本身很纯净,心智模型也不复杂,但实际使用还得搭配redux-thunk、redux-saga、redux-observable这些中间件(middleware)和reselect、immer这样的辅助工具才能达到真正可用的状态

Redux Middleware

Redux中间件的原理差不多,通过中间件的预处理允许View(组件)中dispatch更灵活的action(可以是函数或者promise等),然后在中间件中处理各种副作用(接口请求等)等。但最终都是将action转换为redux需要的plain object(普通对象)格式dispatch到redux store中
React 状态管理 - 图4

Redux-thunk

Redux-thunk允许应用在组件中dispatch一个function(这个function就被称为thunk),原本在组件中的异步代码被抽离到这个thunk中实现,从而在不同组件里复用。

Thunk 是一类函数的别名,这类函数的主要用途是将任务延迟执行,或者给另一个函数执行前后添加一些额外的操作。

如下的原生redux写法:

  1. // 原生Redux用法
  2. import { useCallback, useEffect } from 'react';
  3. import { useDispatch, useSelector } from 'react-redux';
  4. const Demo = () => {
  5. const dispatch = useDispatch();
  6. const fetchUser = useCallback(
  7. async () => {
  8. const result = await getUserApi(params);
  9. dispatch({
  10. type: 'RECEIVE_USER_INFO',
  11. payload: result,
  12. });
  13. },
  14. [dispatch]
  15. );
  16. useEffect(
  17. () => {
  18. fetchUser();
  19. },
  20. [fetchUser]
  21. );
  22. const currentUser = useSelector(state => state?.context?.currentUser);
  23. return <div>{currentUser?.name}</div>;
  24. };

使用Redux-thunk的写法改造上面的例子:

  1. // Redux Thunk Creator
  2. const fetchUser = (params) => {
  3. return async (dispatch, getState) => { // This is a Thunk
  4. const result = await getUserApi(params);
  5. dispatch({
  6. type: 'RECEIVE_USER_INFO',
  7. payload: result,
  8. });
  9. };
  10. }
  11. const Demo = () => {
  12. const dispatch = useDispatch();
  13. useEffect(
  14. () => {
  15. dispatch(fetchUser(params));
  16. },
  17. [dispatch]
  18. );
  19. const currentUser = useSelector(state => state?.context?.currentUser);
  20. return <div>{currentUser?.name}</div>;
  21. };

Mobx - 在React里写Vue

和redux一样,mobx本身是一个UI无关的纯粹的状态管理库,通过mobx-react或更轻量的mobx-react-lite和react建立连接。

心智模型

Mobx的心智模型和react很像,它区分了应用程序的三个概念:

  • State(状态)
  • Actions(动作)
  • Derivations(派生)

首先创建可观察的状态(Observable State),通过Action更新State,然后自动更新所有的派生(Derivations)。派生包括Computed value(类似useMemo或useSelector)、副作用函数(类似useEffect)和UI(render)。
React 状态管理 - 图5
Mobx虽然心智模型像react,但是实现却是完完全全的vue:mutable + proxy(为了兼容性,proxy实际上使用Object.defineProperty实现)。

尤大本人也盖过章:React + MobX 本质上就是一个更繁琐的Vue。
既然这样,直接用Vue不香吗(狗头)。

Mobx vs Redux

Mobx和Redux的对比,实际上可以归结为 面向对象 vs 函数式Mutable vs Immutable

  • 相比于redux的广播遍历dispatch,然后遍历判断引用来决定组件是否更新,mobx基于proxy可以精确收集依赖、局部更新组件(类似vue),理论上会有更好的性能,但redux认为这可能不是一个问题(Won’t calling “all my reducers” for each action be slow?
  • Mobx因为数据只有一份引用,没有回溯能力,不像redux每次更新都相当于打了一个快照,调试时搭配redux-logger这样的中间件,可以很直观地看到数据流变化历史。
  • Mobx的学习成本更低,没有全家桶。
  • Mobx在更新state中深层嵌套属性时更方便,直接赋值就好了,redux则需要更新所有途经层级的引用(当然搭配immer也不麻烦)。

总结

简单场景使用原生的useState、useReducer、useContext就能满足;
Redux高度模板化、分层化,职责划分清晰,塑造了其状态在可回溯、可维护性方面的优势;