【React的做弊模式】理解useReducer的優點和高級用法

或許你已經知道,「當多個state須要一塊兒更新時,就應該考慮使用useReducer」;或許你也已經據說過,「使用useReducer可以提升應用的性能」。可是篇文章但願幫助你理解:爲何useReducer能提升代碼的可讀性和性能,以及如何在reducer中讀取props的值。react

因爲useReducer造就的解耦模式以及高級用法,React團隊的Dan Abramov將useReducer描述爲"React的做弊模式"數組

useReducer的優點

舉一個例子:閉包

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 doHow 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

    • reducer比setState更加擅長描述「如何更新狀態」。好比,reducer可以讀取相關的狀態、同時更新多個狀態。
    • 【組件負責發出action,reducer負責更新狀態】的解耦模式,使得代碼邏輯變得更加清晰。
    • 簡單來記,就是每當編寫setState(prevState => newState)的時候,就應該考慮是否值得將它換成useReducer。
  • 經過傳遞useReducer的dispatch,能夠減小狀態值的傳遞

    • useReducer老是返回相同的dispatch函數,這是完全解耦的標誌:狀態更新邏輯能夠任意變化,而發起actions的渠道始終不變
    • 得益於前面的解耦模式,useEffect函數體、callback function只須要使用dispatch來發出action,而無需直接依賴狀態值。所以在useEffect、useCallback、useMemo的deps數組中無需包含狀態值,也減小了它們更新的須要。不但能提升可讀性,並且能提高性能(useCallback、useMemo的更新每每會形成子組件的刷新)。

高級用法:內聯reducer

你能夠將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的觸發時機的理解是錯誤的(包括之前的我)。我之前理解的觸發時機是這樣:

  1. 某個button被用戶點擊,它的onClick被調用,其中執行了dispatch({type:'add'}),React框架安排一次更新
  2. React框架處理剛纔安排的更新,調用reducer(prevState, {type:'add'}),來獲得新的狀態
  3. React框架用新的狀態來渲染組件樹,渲染到Counter組件的useReducer時,返回上一步獲得的新狀態便可

可是實際上,React會在下次渲染的時候再調用reducer來處理action:

  1. 某個button被用戶點擊,它的onClick被調用,其中執行了dispatch({type:'add'}),React框架安排一次更新
  2. React框架處理剛纔安排的更新,開始重渲染組件樹
  3. 渲染到Counter組件的useReducer時,調用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

參考資料

相關文章
相關標籤/搜索