使用useEffect的注意事項

有時候,你的 effect 可能會使用一些頻繁變化的值。你可能會忽略依賴列表中 state,但這通常會引起 Bug:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1); // 這個 effect 依賴於 `count` state
    }, 1000);
    return () => clearInterval(id);
  }, []); // Bug: `count` 沒有被指定爲依賴

  return <h1>{count}</h1>;
}

傳入空的依賴數組 [],意味着該 hook 只在組件掛載時運行一次,並非重新渲染時。但如此會有問題,在 setInterval 的回調中,count 的值不會發生變化。因爲當 effect 執行時,我們會創建一個閉包,並將 count 的值被保存在該閉包當中,且初值爲 0。每隔一秒,回調就會執行 setCount(0 + 1),因此,count 永遠不會超過 1。

指定 [count] 作爲依賴列表就能修復這個 Bug,但會導致每次改變發生時定時器都被重置。事實上,每個 setInterval 在被清除前(類似於 setTimeout)都會調用一次。但這並不是我們想要的。要解決這個問題,我們可以使用 setState 的函數式更新形式。它允許我們指定 state 該 如何 改變而不用引用 當前 state:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1); // ✅ 在這不依賴於外部的 `count` 變量
    }, 1000);
    return () => clearInterval(id);
  }, []); // 我們的 effect 不適用組件作用域中的任何變量

  return <h1>{count}</h1>;
}

setCount 函數的身份是被確保穩定的,所以可以放心的省略掉)

此時,setInterval 的回調依舊每秒調用一次,但每次 setCount 內部的回調取到的 count 是最新值(在回調中變量命名爲 c)。

在一些更加複雜的場景中(比如一個 state 依賴於另一個 state),嘗試用 useReducer Hook 把 state 更新邏輯移到 effect 之外。 useReducer 的 dispatch 的身份永遠是穩定的 —— 即使 reducer 函數是定義在組件內部並且依賴 props。

const initialState = {count: 0}
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    default:
      throw new Error()
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  useEffect(() => {
    const id = setInterval(() => {
      dispatch({type: 'increment'})
    }, 1000);
    return () => setInterval(id);
  }, []);

  return <h1>{state.count}</h1>;
}

(如果你熟悉 Redux 的話,就已經知道它如何工作了。)

在某些場景下,useReducer 會比 useState 更適用,例如 state 邏輯較複雜且包含多個子值,或者下一個 state 依賴於之前的 state 等。並且,使用 useReducer 還能給那些會觸發深更新的組件做性能優化,因爲你可以向子組件傳遞 dispatch 而不是回調函數 。

function Child (props) {
  console.log('重新渲染')
  function handleClick () {
    props.dispatch({type: 'increment'})
  }
  return (
    <button onClick={handleClick}>按鈕</button>
  )
}

Child = React.memo(Child)

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  function handleClick() {
    dispatch({type: 'increment'})
  }

  return (
    <div>
      <h1 onClick={handleClick}>{state.count}</h1>
      <Child dispatch={dispatch} />
    </div>
  )
}

React 會確保 dispatch 函數的標識是穩定的,並且不會在組件重新渲染時改變。這就是爲什麼可以安全地從 useEffect 或 useCallback 的依賴列表中省略 dispatch。

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  function handleClick() {
    dispatch({type: 'increment'})
  }

  const callback = useCallback(() => {
    dispatch({type: 'increment'})
  }, [])

  return (
    <div>
      <h1 onClick={handleClick}>{state.count}</h1>
      <Child callback={callback} />
    </div>
  )
}

如果直接從useReducer返回操作,則其行爲與useState幾乎相同。

function App() {
  const [name, setName] = useReducer((_, value) => value, '請輸入');
  return (
    <div className="App">
      <input value={name} onChange={e => setName(e.target.value)} />
    </div>
  );
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章