React Hooks 的“闭包陷阱(stale closure)”本质是:函数拿到的是创建时那一刻的 state/props,而不是最新值。常见出现在 setTimeout、setInterval、事件监听、异步请求、useEffect 中。
例如:
import { useState } from 'react';
export default function Demo() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
console.log(count); // 永远打印点击时的 count
}, 3000);
};
return (
<>
<button onClick={() => setCount(count + 1)}>
+1
</button>
<button onClick={handleClick}>
延迟打印
</button>
</>
);
}
假设:
count = 0
点击延迟打印
马上点击 +1 -> count=1
3秒后输出:0
因为 setTimeout 闭包保存的是旧 count。
解决方案1:函数式更新(推荐处理 state 更新)
适合依赖旧值计算新值。
错误:
setCount(count + 1);
setCount(count + 1);
console.log(count); // +1
正确:
setCount(prev => prev + 1);
setCount(prev => prev + 1);
console.log(count); // +2
React 会拿最新状态:
setInterval(() => {
setCount(prev => prev + 1);
},1000);
避免:
setCount(count + 1); // count 永远旧
解决方案2:useRef 保存最新值(最常见)
适合:
- setTimeout
- setInterval
- websocket
- 原生事件
- 防抖节流
例子:
import { useRef, useState, useEffect } from 'react';
export default function Demo() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
const handleClick = () => {
setTimeout(() => {
console.log(countRef.current);
}, 3000);
};
return (
<>
<button onClick={() => setCount(count+1)}>
+1
</button>
<button onClick={handleClick}>
打印
</button>
</>
);
}
始终输出最新值。
原理:
闭包 -> 拿不到最新 state
ref.current -> 永远最新
很多库(例如防抖 hooks)都这么做。
解决方案3:正确维护 useEffect 依赖
错误:
useEffect(() => {
getData(id);
}, []);
即使 id 变化也不会重新执行。
应写:
useEffect(() => {
getData(id);
}, [id]);
遵守 eslint:
react-hooks/exhaustive-deps
不要随便关:
// eslint-disable-next-line
很多闭包问题来自错误依赖。
解决方案4:useCallback + 依赖更新
错误:
const getUser = useCallback(() => {
console.log(id);
}, []);
id 永远旧。
正确:
const getUser = useCallback(() => {
console.log(id);
}, [id]);
解决方案5:抽离自定义 Hook(推荐业务场景)
比如轮询:
错误:
useEffect(() => {
const timer=setInterval(()=>{
fetchData(page);
},1000)
return ()=>clearInterval(timer)
},[])
page 永远旧。
改:
function useLatest(value){
const ref=useRef(value);
useEffect(()=>{
ref.current=value;
},[value])
return ref;
}
使用:
const pageRef = useLatest(page);
useEffect(() => {
const timer = setInterval(() => {
fetchData(pageRef.current);
},1000);
return ()=>clearInterval(timer);
},[])
解决方案6:React 19 的 useEffectEvent(未来推荐)
React 提供了解决闭包问题的新方案:
const onVisit = useEffectEvent(() => {
console.log(count);
});
useEffect(()=>{
window.addEventListener('click',onVisit)
return ()=>{
window.removeEventListener('click',onVisit)
}
},[])
事件始终读取最新 state。
适合:
- 订阅
- 监听器
- effect 内回调
前端开发里可以记一个经验:
需要最新状态:
→ 用 ref
依赖旧状态更新:
→ 用函数式 setState
effect/callback 使用变量:
→ 补全依赖数组
长生命周期回调:
→ useLatest / useRef
React19:
→ useEffectEvent
像你做性能平台、轮询二维码状态、下载任务进度这些场景,setInterval + useRef 基本是闭包问题高发区。