或許你已經知道,「當多個state須要一塊兒更新時,就應該考慮使用useReducer」;或許你也已經據說過,「使用useReducer可以提升應用的性能」。可是篇文章但願幫助你理解:爲何useReducer能提升代碼的可讀性和性能,以及如何在reducer中讀取props的值。react
因爲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之後:ide
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封裝了「如何更新狀態」的邏輯:ui
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(); } }
總結:es5
當狀態更新邏輯比較複雜的時候,就應該考慮使用useReducer。由於:spa
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