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没有优劣之外,它们有各自适用的场景,这里有如下建议:
- 当状态是一个拥有很多属性的复杂对象,并且状态更新涉及复杂的逻辑时,推荐使用useReducer。
- 当某个状态的更新受另一个状态影响时,推荐使用 useReducer将它们放在一起。
- 当状态只是单独的基本数据类型时,推荐使用 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。
推荐阅读
原文链接:https://juejin.cn/post/7244876679041794105 作者:何遇er