iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
Modern Web

30 天掌握 React & Next.js:從基礎到面試筆記系列 第 14

Day 14:避免 stale closure — 正確使用依賴陣列、functional updater、useRef

  • 分享至 

  • xImage
  •  

在 Day 13,我們學到 每次 render 都會捕捉當下的 props 和 state。這會導致 callback 使用的值是「舊的」,形成 stale closure(過期閉包) 問題。
今天要來了解以下五個技巧來避免這些陷阱,並且讓副作用(effects)更可控、更安全。

核心觀念

  1. 依賴陣列(dependency array)

    • 原則:effect 讀取什麼值,就要把它放進 deps。
    • 漏寫依賴 → effect 不重跑 → callback 繼續用舊值。
    • 過多依賴(特別是每次都變的新函式/物件) → effect 無限重跑。
    • 技巧:使用 useCallbackuseMemo 穩定引用,避免不必要的重跑。
  2. stale closure(過期閉包)

    • callback 捕捉到的是某次 render 的快照。
    • 當 state 更新但 effect 沒重跑,callback 就會一直用過期值。
    • 常見於 setIntervalsetTimeout、或事件 handler。
  3. functional updater

    • setState(prev => prev + 1) → React 會保證給你最新的值。
    • 適合純粹更新 state 的場景。
    • 好處:可以讓 effect 減少依賴,因為不需要直接引用舊 state。
  4. useRef 儲存最新 callback / 值

    • 當 effect 只想 setup 一次(例如建立 interval),但內部邏輯要持續更新。
    • 每次 render 更新 ref.current,interval callback 永遠執行最新邏輯。
    • 適合 callback 複雜或涉及多個值的場景。
  5. clean up / 非同步操作

    • effect 在重跑或 unmount 時需要清理:

      • 清除 timer (clearInterval, clearTimeout)
      • 取消 fetch 請求(AbortController
      • 解除事件監聽或訂閱
    • 目標:避免 memory leak 或過時的 callback 去更新 state。

範例程式碼

錯誤:closure 捕捉舊值

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

  React.useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1); // 這裡的 count 可能永遠是舊值
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <div>Count: {count}</div>;
}

方法一:functional updater

React.useEffect(() => {
  const id = setInterval(() => {
    setCount(prev => prev + 1); // 永遠拿最新 state
  }, 1000);
  return () => clearInterval(id);
}, []);

方法二:useRef 儲存 callback

function Counter() {
  const [count, setCount] = React.useState(0);
  const callbackRef = React.useRef();

  React.useEffect(() => {
    callbackRef.current = () => setCount(c => c + 1);
  });

  React.useEffect(() => {
    const id = setInterval(() => callbackRef.current(), 1000);
    return () => clearInterval(id);
  }, []);

  return <div>Count: {count}</div>;
}

方法三:清理非同步操作

function DataFetcher({ url }) {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    const controller = new AbortController();

    fetch(url, { signal: controller.signal })
      .then(r => r.json())
      .then(setData)
      .catch(err => {
        if (err.name !== 'AbortError') console.error(err);
      });

    return () => controller.abort(); // cleanup
  }, [url]);

  return <pre>{JSON.stringify(data)}</pre>;
}

常見陷阱

  • 忘記加依賴 → callback 用舊值
  • 把 unstable 函式/物件直接放進 deps → 無限重跑
  • 以為改 ref.current 會 rerender(其實不會)
  • 忘記清理 → 記憶體洩漏 / race condition

小練習

  1. 嘗試用 setTimeout 做一個延遲三秒更新 state 的功能,觀察有沒有 stale closure。
  2. 把某個需要多個 state 的 interval 重構成 useRef 方案。

面試回答

中文

在 React 中,useEffect 裡的 callback 會捕捉當次 render 的值,如果不小心就會讀到舊值。解法有幾個:

  • 用 functional updater 拿到最新 state。
  • 或用 useRef 儲存最新 callback,讓長期存在的副作用可以呼叫最新邏輯。
  • 最重要的是依賴陣列要寫正確,還要記得清理非同步操作,避免 race condition。

英文

In React, closures can cause callbacks to read old state. To fix this, I usually pick one of three tools. If I just need to update state, I use a functional updater. If I want a stable effect setup, like a timer, I use a ref to hold the latest callback. And in any case, I pay attention to the dependency array and make sure I clean up async operations like fetch or subscriptions.

總結

  • Day 13:理解 closure 捕捉 render 值 → 會有 stale closure
  • Day 14:學會解決 → 依賴陣列、functional updater、useRef、清理非同步

上一篇
Day 13 : React 中 render 時捕捉的值 & 閉包行為
系列文
30 天掌握 React & Next.js:從基礎到面試筆記14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言