useEffect 闭包陷阱
理解 React useEffect 中的闭包问题及解决方案
问题
在 React 中使用 useEffect 时,回调函数中访问的 state 或 props 可能是过时的值,这就是闭包陷阱。
解答
问题复现
import { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
// 定时器回调捕获了初始的 count 值 (0)
const timer = setInterval(() => {
console.log('当前 count:', count); // 永远输出 0
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖数组,effect 只执行一次
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
点击按钮后,页面显示的数字在增加,但控制台始终输出 0。
原因分析
// useEffect 的回调在组件首次渲染时创建
// 此时 count = 0,回调函数形成闭包,捕获了这个值
useEffect(() => {
// 这个函数"记住"了创建时的 count 值
const timer = setInterval(() => {
console.log(count); // 闭包中的 count 永远是 0
}, 1000);
return () => clearInterval(timer);
}, []);
解决方案
方案一:添加依赖
useEffect(() => {
const timer = setInterval(() => {
console.log('当前 count:', count);
}, 1000);
return () => clearInterval(timer);
}, [count]); // count 变化时重新创建定时器
缺点:每次 count 变化都会清除并重建定时器。
方案二:使用 useRef
import { useState, useEffect, useRef } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
// 同步更新 ref
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
const timer = setInterval(() => {
// ref.current 始终是最新值
console.log('当前 count:', countRef.current);
}, 1000);
return () => clearInterval(timer);
}, []);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
方案三:函数式更新(适用于需要基于旧值更新的场景)
useEffect(() => {
const timer = setInterval(() => {
// 函数式更新可以拿到最新的 state
setCount(prevCount => {
console.log('当前 count:', prevCount);
return prevCount + 1;
});
}, 1000);
return () => clearInterval(timer);
}, []);
方案四:使用 useCallback + 依赖
import { useState, useEffect, useCallback } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const logCount = useCallback(() => {
console.log('当前 count:', count);
}, [count]);
useEffect(() => {
const timer = setInterval(logCount, 1000);
return () => clearInterval(timer);
}, [logCount]);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
关键点
- 闭包陷阱的本质:函数创建时捕获了当时的变量值,后续变量更新不会影响已捕获的值
- useRef 的
.current是可变的,不会触发重渲染,适合存储需要跨渲染周期访问的最新值 - 函数式更新
setState(prev => ...)可以获取最新 state,但只适用于需要更新 state 的场景 - 正确设置依赖数组是最直接的解决方案,但要注意可能带来的性能影响
- 事件监听、定时器、订阅等场景最容易遇到闭包陷阱
目录