一文带你搞懂React中的useReducer
作者:何遇er
useReducer 是除useState之外另一个与状态管理相关的 hook,对于熟悉 Redux 的工程师而言,理解 useReducer 将变得简单,在 React 内部,useState 由 useReducer 实现。
useReducer 的类型定义
useReducer 的类型定义如下:
function useReducer<R extends ReducerWithoutAction<any>, I>( reducer: R, initializerArg: I, initializer: (arg: I) => ReducerStateWithoutAction<R> ): [ReducerStateWithoutAction<R>, DispatchWithoutAction]; function useReducer<R extends ReducerWithoutAction<any>>( reducer: R, initializerArg: ReducerStateWithoutAction<R>, initializer?: undefined ): [ReducerStateWithoutAction<R>, DispatchWithoutAction]; function useReducer<R extends Reducer<any, any>, I>( reducer: R, initializerArg: I & ReducerState<R>, initializer: (arg: I & ReducerState<R>) => ReducerState<R> ): [ReducerState<R>, Dispatch<ReducerAction<R>>]; function useReducer<R extends Reducer<any, any>, I>( reducer: R, initializerArg: I, initializer: (arg: I) => ReducerState<R> ): [ReducerState<R>, Dispatch<ReducerAction<R>>]; function useReducer<R extends Reducer<any, any>>( reducer: R, initialState: ReducerState<R>, initializer?: undefined ): [ReducerState<R>, Dispatch<ReducerAction<R>>];
useReducer 的类型定义很复杂,一共有 5 个重载,总体而言,它最多接受3个参数,第 1 个参数是一个用于更新状态的函数,之后将它称为 reducer。第 3 个参数非必填,如果不存在第 3 个参数,那么第 2 个参数将作为状态的初始值;如果存在第 3 个参数,那么它必须是函数,此时第 2 个参数被传递给该函数用于计算状态的初始值,该函数只在组件初始渲染时执行一次。useReducer 的返回值是一个长度为 2 的数组,数组的第 1 个位置是状态值,第 2 个位置是一个用于触发状态更新的函数,将它记为dispatch,调用 dispatch 将导致 reducer 被调用。接下来通过计数器 demo 对比 useState 和useReducer 用法上的差异。
用 useState 实现计数器
用 useState 实现计数器,代码如下:
function UseStateCounterDemo() { const [value, setValue] = useState<number>(0) const [step, setStep] = useState<number>(1) const onChangeStep = (ev: React.ChangeEvent<HTMLInputElement>) => { setStep(Number(ev.target.value)) } return ( <div> <h2>用useState实现计数器</h2> count: {count}; step: <input type='number' value={step} onChange={onChangeStep}/> <button onClick={() => setValue(value + step)}>加</button> <button onClick={() => setValue(value - step)}>减</button> <button onClick={() => {setValue(0); setStep(1)}}>重置</button> </div> ) }
上述代码很简单,它使用 useState 定义了两个状态,分别表示计数器的值和加减步数,在过去的文章里已对 useState 做过详细的介绍,这里不再赘述,下面详细介绍用 useReducer 实现计数器。
用 useReducer 实现计数器
定义 reducer
使用 useReducer hook 离不开reducer,它是一个函数,用于更新 useReducer 返回的状态,代码如下:
const initArg: Counter = {value: 0, step: 1} // 它用于更新state function reducer(prevState: Counter, action: Action): Counter { switch (action.type) { case 'increment': return { ...prevState, value: prevState.value + prevState.step } case 'decrement': return { ...prevState, value: prevState.value - prevState.step } case 'reset': return initArg case 'changeStep': return { ...prevState, step: action.value || initArg.value } default: throw new Error(); } }
reducer 用于更新状态,计数器 demo 有两个状态,分别是计数器的当前值和它的加减步数,这两个状态密切相关,这里用一个 TS 接口去描述它,代码如下:
interface Counter { // 计数器的当前值 value: number // 计数器的加减步数 step: number }
计数器有 3 种操作,分别是加、减、重置和修改步数,这里用 TS 接口描述这些行为,代码如下:
interface Action { type: 'increment' | 'decrement' | 'reset'|'changeStep', value?: number }
在组件中使用 useReducer
// 计数器的初始值 const initArg: Counter = {value: 0, step: 1} function UseReducerCounterDemo() { // 使用上一步定义 reducer const [counter, dispatch] = useReducer(reducer, initArg) const onChangeStep = (ev: React.ChangeEvent<HTMLInputElement>) => { dispatch({type: 'changeStep',value: Number(ev.target.value)}) } return ( <div> <h2>用useReducer实现计数器</h2> count: {counter.value}; step: <input type='number' value={counter.step} onChange={onChangeStep}/> <button onClick={() => dispatch({type: 'increment'})}>加</button> <button onClick={() => dispatch({type: 'decrement'})}>减</button> <button onClick={() => dispatch({type: 'reset'})}>重置</button> </div> ) }
只考虑代码量,读者应该都会认为useState比useReducer更简洁。仔细观察可以发现,上述计数器除了有value还有step,step对value有影响,UseStateCounterDemo组件将它们零散地保存在不同的状态中,UseStateCounterDemo组件将它们关联在同一个状态中,内聚性更高。useState与useReducer没有优劣之外,它们有各自适用的场景,这里有如下建议:
1.当状态是一个拥有很多属性的复杂对象,并且状态更新涉及复杂的逻辑时,推荐使用useReducer。
2.当某个状态的更新受另一个状态影响时,推荐使用 useReducer将它们放在一起。
3.当状态只是单独的基本数据类型时,推荐使用 useState。
在介绍 useEffect时曾强调,为了在 effect 中拿到状态最新的值,必须给 effect 设置正确地依赖项。在 useEffect 中使用 useReducer 返回的 dispatch 能让 effect 自给自足,减少依赖项。示例代码如下:
function DispatchDemo(props: {step: number}) { function reducer(value: number) { // 始终能访问到最新的 step return props.step + value } const [value, dispatch] = useReducer(reducer, 0) useEffect(() => { document.body.addEventListener('click', dispatch) return () => { document.body.removeEventListener('click', dispatch) } }, []) return // todo }
上述代码 useEffect 的第二个参数为空数组,这意味着 effect 只在组件初始渲染时执行。由于React 会让 dispatch 在组件的每次渲染中保持唯一的引用,所以 dispatch 不必出现在effect的依赖中,此特性与 ref 类似。虽然 dispatch 的引用保持不变,但它能调用组件本次渲染时的reducer,在 reducer 作用域将得到最新的 state 和 props。
推荐阅读:
我搞懂了 React 的 useState 和 useEffect
到此这篇关于一文带你搞懂React中的useReducer的文章就介绍到这了,更多相关React useReducer内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!