前言
useReducer是useState的替代方案。它接收一个形如(state, action) => newState的reducer,并返回当前的state以及与其配套的dispatch方法。(如果你熟悉Redux的话,就已经知道它如何工作了。)
使用场景
-
在某些场景下,
useReducer会比useState更适用。 例如state逻辑复杂且包含多个子值(如state的值为一个对象),或者下一个state依赖于之前的state等。 并且,使用useReducer还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递dispatch而不是回调函数。 -
useState更适合用于state值较为单一的状态
例子
用useState来写计数器:
function Example() {
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}
用reducer重写useState的计数器示例:
// 定义初始值
const initialState = {count: 0}
// 定义reducer函数,参数为state值和action动作,想要改变state的特定值的操作都放在reducer中
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() {
// 初始化userReducer,参数为定义好的reducer函数和initialState(初始状态值)
const [state, dispatch] = useReducer(reducer, initialState)
return (
<>
Count: {state.count}
// 通过调用dispatch传入action,从而改变state
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'incerment'})}>+</button>
</>
)
}
对比:useState传入的参数为初始状态值,userReducer传入了reducer和初始状态值。
useState返回的两个值count和setCount,userReducer返回的两个值为state和 dispatch。其中state为count的升级版,dispatch为setCount的升级版。dispatch的第一个参数为action,有些情况下我们还需要将修改后的值传入到state中,此时我们可以使用第二个参数payload将改变后值传入state中。
注:React会确保dispatch函数的标识是稳定的,并且不会再组件重新渲染时改变。这就是为什么可以安全地从seEffect或useCallback的依赖中省略dispatch。
指定初始state
有两种不同初始化 useReducer state 的方式,你可以根据使用场景选择其中的一种。将初始state作为第二个参数传入useReducer是最简单的方法:
const [state, dispatch] = useReducer(
reducer,
{count: initialCount}
)
注意:React不使用state = initialState 这一由 Redux推广开来的参数约定。有时候初始值依赖于props,因此需要在调用Hook时指定。如果你特别喜欢上述的参数约定,可以通过调用useReducer(reducer, undefined, reducer)来模拟Redux的行为,但我们不鼓励你这么做。
惰性初始化
你可以选择惰性地创建初始state。为此,需要将init函数作为useReducer的第三个参数传入,这样初始state将被设置为init(initalArg)。
这么做可以将用于计算state的逻辑提取到reducer外部,这也为将来对重置state的action做处理提供了便利:
function init(initialCount) {
return {count: initialCount}
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}
function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init)
return (
<>
Count: {state.count}
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
)
}
跳过dispatch
如果Reducer Hook的返回值与当前state相同,React将跳过子组件的渲染及副作用的执行。(React使用Object.is比较算法来比较state。)
需要注意的是,React可能仍需要跳过渲染前再次渲染该组件。不过由于React不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用useMemo来进行优化。