在 React Hooks 中的 闭包陷阱(Closure Trap)在 useEffect
、事件回调、定时器等场景里很常见。
1. 闭包陷阱是什么
- 当你在函数组件里定义一个回调(比如事件处理函数),这个回调会捕获当时渲染时的变量值。
- 如果后面状态更新了,但回调里引用的仍然是旧的变量值(因为它闭包捕获的是旧值),就会出现状态不同步的问题。
2. 典型例子
import React, { useState, useEffect } from "react";
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 总是打印 0(闭包陷阱)
setCount(count + 1); // 永远基于旧值
}, 1000);
return () => clearInterval(timer);
}, []); // ❌ 空依赖数组,count 不会更新
return <h1>{count}</h1>;
}
现象:
- 你期望每秒加 1,但实际
count
永远停在 1 或只打印旧值。 - 原因:
useEffect
只在首次渲染执行一次,所以定时器回调里捕获的是第一次渲染时的 count。
3. 为什么会发生
- React 函数组件每次渲染都是一个新的执行上下文。
- 变量值是“渲染快照” ,渲染完成后不会自动更新到已创建的闭包中。
- 当回调函数使用了上一次渲染的变量,就会变成“旧值引用”。
4. 常见触发场景
场景 | 问题原因 |
---|---|
setInterval 、setTimeout |
定时器回调捕获了旧状态 |
事件回调 | 绑定时的函数引用了旧值 |
异步请求回调 | then/callback 捕获了旧状态 |
WebSocket、监听器 | 回调绑定后状态不会自动刷新 |
5. 解决方案
方案 1:依赖数组声明最新状态
useEffect(() => {
const timer = setInterval(() => {
console.log(count);
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, [count]); // ✅ 每次 count 变化时重新绑定定时器
缺点:可能频繁解绑/绑定监听器。
方案 2:使用函数式更新
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1); // ✅ 始终基于最新值
}, 1000);
return () => clearInterval(timer);
}, []); // 依赖数组可以为空
优势:避免闭包陷阱,保持依赖稳定。
方案 3:使用 useRef
存储最新值
const countRef = useRef(count);
useEffect(() => {
countRef.current = count; // ✅ 每次渲染更新最新值
});
useEffect(() => {
const timer = setInterval(() => {
console.log(countRef.current); // 始终是最新值
setCount(c => c + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
适合定时器、事件监听等需要稳定回调的场景。
方案 4:使用 useCallback
保证函数引用稳定
const handleClick = useCallback(() => {
console.log(count); // count 会更新,因为依赖变了
}, [count]);
不过这会导致依赖变化时重新生成函数引用,适合事件处理但不适合频繁绑定解绑的监听。
6. 一句话总结
React Hooks 中的闭包陷阱 = 回调函数捕获了旧的状态值,导致逻辑和 UI 不同步。
核心解决思路:要么让回调用到的状态实时更新(函数式更新 / ref) ,要么确保回调重新生成(依赖数组) 。