React 状态管理
React的状态管理主要分三类:Local state、Context、第三方库。
Local State
local state 的管理就是 useState 和 useReducer 的天下了。
useState - 更细粒度的状态
类组件:将所有的state都放在一个对象中
useState:将组件内的状态再拆分,以更细的粒度维护
import {useCallback, useState} from 'react';
const Foo = () => {
const [stateA, setStateA] = useState(0);
const [stateB, setStateB] = useState(0);
const handleAdd = useCallback(
() => { setStateA(prev => prev + 1); },
[]
);
const handleSubtract = useCallback(
() => { setStateB(prev => prev - 1); },
[]
);
return (
<>
<Button onClick={handleAdd}>{stateA}</Button>
<Button onClick={handleSubtract}>{stateB}</Button>
</>
);
};
useReducer - 复杂逻辑抽象和复用
使用useReducer可以认为是在组件内生成一个独立的redux store,并且这个reducer的逻辑可以在不同的组件中复用。
当state的计算逻辑比较复杂,或者派生状态的变化存在共性,或者reducer逻辑可以复用时,可以优先考虑用useReducer。
例如常见的列表分页逻辑封装:
import {useReducer} from 'react';
const initialState = { pageNum: 1, pageSize: 15 };
const reducer = (state, action) => {
switch (action.type) {
case 'next': // 下一页
return { ...state, pageNum: state.pageNum + 1 };
case 'prev': // 前一页
return { ...state, pageNum: state.pageNum - 1 };
case 'changePage': // 跳转到某页
return { ...state, pageNum: action.payload };
case 'changeSize': // 更改每页展示条目数
return { pageNum: 1, pageSize: action.payload };
default:
return state;
}
};
const Page = () => {
const [pager, dispatch] = useReducer(reducer, initialState);
return (
<Table
pageNum={pager.pageNum}
pageSize={pager.pageSize}
onGoNext={() => dispatch({ type: 'next' })}
onGoPrev={() => dispatch({ type: 'prev' })}
onPageNumChange={(num) => dispatch({ type: 'changePage', payload: num })}
onPageSizeChange={(size) => dispatch({ type: 'changeSize', payload: size })}
/>
);
};
使用 useReducer 还有一个优点是可以优化深层 子组件 需要触发更新时的应用性能。
假设我们在父组件定义了一个state,在子组件中有要更改父组件state的需求,以往惯用的做法是在父组件定义相关的callback,然后一层层地透传给子组件。
在 组件层级特别深 和 callback特别多 的时候,就会回想起被prop透传支配的恐惧,写子组件prop的类型也要脱半层皮。
并且使用useCallback封装的方法有可能因为依赖的变量更新和返回新的引用,而导致透传途径的子组件都可能触发更新。
使用useReducer的话,可以结合useContext,只把dispatch传到子组件中。并且dispatch生成之后引用恒定不变,不会触发context可能的force update。
import {createContext, useReducer, useContext} from 'react';
const ParentDispatch = createContext(null);
const Parent = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<ParentDispatch.Provider value={dispatch}>
<DeepTree parentState={state} />
</ParentDispatch.Provider>
);
};
// 深层子组件
const DeepChild = () => {
const dispatch = useContext(ParentDispatch);
const handleClick = () => {
dispatch({ type: 'add', payload: 'hello' });
};
return <button onClick={handleClick}>Add</button>;
};
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状态管理库莫过于Redux、Mobx、Recoil,其中Redux和Mobx都是老牌强手代表,Recoil则是这两年最火的后起之秀。
Redux的设计是为以下原则服务的:要让状态的变化可追踪,可重复,可维护,因此才会有 reducer, action, middleware 这些概念。
Redux
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中。
Redux-thunk
Redux-thunk允许应用在组件中dispatch一个function(这个function就被称为thunk),原本在组件中的异步代码被抽离到这个thunk中实现,从而在不同组件里复用。
Thunk 是一类函数的别名,这类函数的主要用途是将任务延迟执行,或者给另一个函数执行前后添加一些额外的操作。
如下的原生redux写法:
// 原生Redux用法
import { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
const Demo = () => {
const dispatch = useDispatch();
const fetchUser = useCallback(
async () => {
const result = await getUserApi(params);
dispatch({
type: 'RECEIVE_USER_INFO',
payload: result,
});
},
[dispatch]
);
useEffect(
() => {
fetchUser();
},
[fetchUser]
);
const currentUser = useSelector(state => state?.context?.currentUser);
return <div>{currentUser?.name}</div>;
};
使用Redux-thunk的写法改造上面的例子:
// Redux Thunk Creator
const fetchUser = (params) => {
return async (dispatch, getState) => { // This is a Thunk
const result = await getUserApi(params);
dispatch({
type: 'RECEIVE_USER_INFO',
payload: result,
});
};
}
const Demo = () => {
const dispatch = useDispatch();
useEffect(
() => {
dispatch(fetchUser(params));
},
[dispatch]
);
const currentUser = useSelector(state => state?.context?.currentUser);
return <div>{currentUser?.name}</div>;
};
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)。
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高度模板化、分层化,职责划分清晰,塑造了其状态在可回溯、可维护性方面的优势;