或许你已经知道,“当多个state需要一起更新时,就应该考虑使用useReducer”;或许你也已经听说过,“使用useReducer能够提高应用的性能”。但是篇文章希望帮助你理解:为什么useReducer能提高代码的可读性和性能,以及如何在reducer中读取props的值。
由于useReducer造就的解耦模式以及高级用法,React团队的Dan Abramov将useReducer描述为"React的作弊模式"。
举一个例子:
function Counter() { const [count, setCount] = useState(0); const [step, setStep] = useState(1); useEffect(() => { const id = setInterval(() => { setCount(c => c + step); // 依赖其他state来更新 }, 1000); return () => clearInterval(id); // 为了保证setCount中的step是最新的, // 我们还需要在deps数组中指定step }, [step]); return ( <> <h1>{count}</h1> <input value={step} onChange={e => setStep(Number(e.target.value))} /> </> ); }
这段代码能够正常工作,但是随着相互依赖的状态变多,setState中的逻辑会变得很复杂,useEffect的deps数组也会变得更复杂,降低可读性的同时,useEffect重新执行时机变得更加难以预料。
使用useReducer替代useState以后:
function Counter() { const [state, dispatch] = useReducer(reducer, initialState); const { count, step } = state; useEffect(() => { const id = setInterval(() => { dispatch({ type: 'tick' }); }, 1000); return () => clearInterval(id); }, []); // deps数组不需要包含step return ( <> <h1>{count}</h1> <input value={step} onChange={e => setStep(Number(e.target.value))} /> </> ) }
现在组件只需要发出action,而无需知道如何更新状态。也就是将What to do与How to do解耦。彻底解耦的标志就是:useReducer总是返回相同的dispatch函数(发出action的渠道),不管reducer(状态更新的逻辑)如何变化。
这是useReducer的逆天之处之一,下面会详述
另一方面,step的更新不会造成useEffect的失效、重执行。因为现在useEffect依赖于dispatch,而不依赖于状态值(得益于上面的解耦模式)。这是一个重要的模式,能用来避免useEffect、useMemo、useCallback需要频繁重执行的问题。
以下是state的定义,其中reducer封装了“如何更新状态”的逻辑:
const initialState = { count: 0, step: 1, }; function reducer(state, action) { const { count, step } = state; if (action.type === 'tick') { return { count: count + step, step }; } else if (action.type === 'step') { return { count, step: action.step }; } else { throw new Error(); } }
总结:
当状态更新逻辑比较复杂的时候,就应该考虑使用useReducer。因为:
setState(prevState => newState)
的时候,就应该考虑是否值得将它换成useReducer。通过传递useReducer的dispatch,可以减少状态值的传递。
你可以将reducer声明在组件内部,从而能够通过闭包访问props、以及前面的hooks结果:
function Counter({ step }) { const [count, dispatch] = useReducer(reducer, 0); function reducer(state, action) { if (action.type === 'tick') { // 可以通过闭包访问到组件内部的任何变量 // 包括props,以及useReducer之前的hooks的结果 return state + step; } else { throw new Error(); } } useEffect(() => { const id = setInterval(() => { dispatch({ type: 'tick' }); }, 1000); return () => clearInterval(id); }, []); return <h1>{count}</h1>; }
这个能力可能会出乎很多人的意料。因为大部分人对reducer的触发时机的理解是错误的(包括以前的我)。我以前理解的触发时机是这样:
dispatch({type:'add'})
,React框架安排一次更新reducer(prevState, {type:'add'})
,来得到新的状态但是实际上,React会在下次渲染的时候再调用reducer来处理action:
dispatch({type:'add'})
,React框架安排一次更新reducer(prevState, {type:'add'})
,处理之前的action重要的区别在于,被调用的reducer是本次渲染的reducer函数,它的闭包捕获到了本次渲染的props。
如果按照上面的错误理解,被调用的reducer是上次渲染的reducer函数,它的闭包捕获到上次渲染的props(因为本次渲染还没开始呢)
事实上,如果你简单地使用console.log来打印执行顺序,会发现reducer是在新渲染执行useReducer的时候被同步执行的:
console.log("before useReducer"); const [state, dispatch] = useReducer(reducer, initialState); console.log("after useReducer", state); function reducer(prevState, action) { // these current state var are not initialized yet // would trigger error if not transpiled to es5 var console.log("reducer run", state, count, step); return prevState; }
调用dispatch以后会输出:
before useReducer reducer undefined undefined undefined after useReducer {count: 1, step: 1}
证明reducer确实被useReducer同步地调用来获取新的state。
codesandbox demo