在 Day 13,我們學到 每次 render 都會捕捉當下的 props 和 state。這會導致 callback 使用的值是「舊的」,形成 stale closure(過期閉包) 問題。
今天要來了解以下五個技巧來避免這些陷阱,並且讓副作用(effects)更可控、更安全。
依賴陣列(dependency array)
useCallback
或 useMemo
穩定引用,避免不必要的重跑。stale closure(過期閉包)
setInterval
、setTimeout
、或事件 handler。functional updater
setState(prev => prev + 1)
→ React 會保證給你最新的值。useRef 儲存最新 callback / 值
ref.current
,interval callback 永遠執行最新邏輯。clean up / 非同步操作
effect 在重跑或 unmount 時需要清理:
clearInterval
, clearTimeout
)AbortController
)目標:避免 memory leak 或過時的 callback 去更新 state。
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>;
}
React.useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev + 1); // 永遠拿最新 state
}, 1000);
return () => clearInterval(id);
}, []);
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>;
}
ref.current
會 rerender(其實不會)setTimeout
做一個延遲三秒更新 state 的功能,觀察有沒有 stale closure。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.