iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Modern Web

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

Day 13 : React 中 render 時捕捉的值 & 閉包行為

  • 分享至 

  • xImage
  •  

你可能遇過這樣的場景:在 useEffectsetTimeout、或 setInterval 的 callback 裡,讀到的 state / props 好像老是某種舊值,更新後還是那個值沒變。這不是 React 壞,而是因為在那次 render 裡,callback 已經「捕捉」了那時的值。今天我們來釐清這個捕捉機制,以及怎麼避免拿到舊值。

核心觀念

在 React 中,每次 render 執行時,component 的 props 和 state 在那一輪裡是被固定使用的。當你在該輪 render 裡定義 callback(例如在 useEffectsetIntervalsetTimeout、事件處理器內部),那個 callback 會“捕捉”當時那一輪的 props / state 值。即使之後 component rerender,那個 callback 裡看到的值仍是最初被捕捉的那一輪所用的值。

換句話說:callback 定義時讀到的 props / state,是那一次 render 的值,不會自動跟後續更新同步。

範例程式碼與解說

錯誤範例:setInterval 捕捉舊值

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

  React.useEffect(() => {
    const id = setInterval(() => {
      // 這裡的 callback 捕捉了 count = 0(初始 render 的值)
      setCount(count + 1);
    }, 1000);

    return () => clearInterval(id);
  }, []);  // 空依賴

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

即使 count 持續更新,那個 interval callback 還是用最初被捕捉的 count = 0 去計算。

正確版本:使用 functional updater 避免捕捉舊值

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

  React.useEffect(() => {
    const id = setInterval(() => {
      // 使用 functional updater,不依賴 closure 捕捉的 count
      setCount(c => c + 1);
    }, 1000);

    return () => clearInterval(id);
  }, []);

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

這裡 callback 裡的邏輯不是使用 closure 裡的那個 count,而是 React 每次提供的最新值 c。所以即使 callback 捕捉舊的 closure,也能正確更新。

另一種變體是用 ref 保存 callback:

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

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

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

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

這樣就把 callback 的定義與 interval 執行分開,避免捕捉舊值的問題。

常見錯誤 / 誤解

  • 誤以為 useEffect(fn, []) 就自動同步最新 state → 事實上 callback 捕捉的是該次 render 的值。
  • 認為 callback 裡的值會「自動更新」是誤解 closure 行為。

小練習

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

  React.useEffect(() => {
    const id = setInterval(() => {
      console.log("Later:", count);
    }, 2000);
    return () => clearInterval(id);
  }, []);

  return <button onClick={() => setCount(c => c + 1)}>+1</button>;
}

若你點按讓 count 變成 5,那麼幾秒後 console.log("Later:", count) 印出的值是什麼?為什麼?
答案: 它會印出那次定時器 callback 被創建時讀到的值,例如 0,而不是後續更新後的值,因為 callback 捕捉的是當時那輪 render 的 props / state。

面試回答模板

中文版

在 React 裡,每次 render 執行時,props 和 state 都會被 callback(像是在 useEffectsetInterval 裡定義的函式)讀取並「捕捉(capture)」當下的值。那個 callback 後續永遠都是用被捕捉的值,不會自動看到後續更新的 state 或 props。使用 functional updater 或 refs 可以讓 callback 能拿到最新的狀態,而不是用舊捕捉到的值。

英文版

In React, when a render runs, props and state are read and “captured” by callbacks (like in useEffect or setInterval) at that moment. That callback will always refer to those captured values — it doesn’t automatically see later updates. Using functional updater or refs helps ensure the callback works with the current state, not old captured values.

總結

  • 每次 render 裡的 props / state 被當作當下值給 callback 捕捉;
  • callback 捕捉的是該輪 render 的值,不會自動同步後續更新;
  • 使用 functional updater 或 ref 技巧可以避免 callback 捕捉舊值的問題。

上一篇
Day 12:useState vs useReducer — 什麼時候該用哪一個?
系列文
30 天掌握 React & Next.js:從基礎到面試筆記13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言