看这样一个组件,通过定时器不断的累加 count:
js
import { useEffect, useState } from 'react';
function App() {
const [count,setCount] = useState(0);
useEffect(() => {
setInterval(() => {
console.log(count);
setCount(count + 1);
}, 1000);
}, []);
return <div>{count}</div>
}
export default App;
你觉得这个 count 会每秒加 1 么? 不会。
可以看到,setCount 时拿到的 count 一直是 0。
为什么呢?
大家可能觉得,每次渲染都引用最新的 count,然后加 1,所以觉得没问题:

但是,现在 useEffect 的依赖数组是 [],也就是只会执行并保留第一次的 function。
而第一次的 function 引用了第一次渲染的 count,当第二次渲染的时候,取的count还是第一次function里面的count,所以形成了闭包,使用了自由变量(不是在自己函数内部定义的变量)就是闭包。
也就是实际上的执行是这样的:

这就导致了每次执行定时器的时候,都是在 count = 0 的基础上加一。
这就叫做 hook 的闭包陷阱。
那怎么解决这个问题呢?不让它形成闭包不就行了?
这时候可以用 setState 的另一种参数:
js
useEffect(() => {
setInterval(() => {
console.log(count);
setCount(count => count + 1);
}, 1000);
}, []);
这次并没有形成闭包,每次的 count 都是参数传入的上一次的 state。
但有的时候,是必须要用到 state 的,也就是肯定会形成闭包,
比如这里,console.log 的 count 就用到了外面的 count,形成了闭包,但又不能把它挪到 setState 里去写:
js
useEffect(() => {
setInterval(() => {
console.log(count);
setCount(count => count + 1);
}, 1000);
}, []);
这种情况怎么办呢?
还记得 useEffect 的依赖数组是干啥的么?当依赖变动的时候,会重新执行 effect。
所以可以这样:
js
import { useEffect, useState } from 'react';
function App() {
const [count,setCount] = useState(0);
useEffect(() => {
console.log(count);
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => {
clearInterval(timer);
}
}, [count]);
return <div>{count}</div>
}
export default App;
依赖数组加上了 count,这样 count 变化的时候重新执行 effect,那执行的函数引用的就是最新的 count 值。
但是,有定时器不能重新跑 effect 函数,那怎么做呢?
可以用 useRef。
js
import { useEffect, useState, useRef, useLayoutEffect } from 'react';
function App() {
const [count, setCount] = useState(0);
const updateCount = () => {
setCount(count + 1);
};
const ref = useRef(updateCount);
ref.current = updateCount;
useEffect(() => {
const timer = setInterval(() => ref.current(), 1000);
return () => {
clearInterval(timer);
}
}, []);
return <div>{count}</div>;
}
export default App;
通过 useRef 创建 ref 对象,保存执行的函数,每次渲染更新 ref.current 的值为最新函数。
这样,定时器执行的函数里就始终引用的是最新的 count。
useEffect 只跑一次,保证 setIntervel 不会重置,是每秒执行一次。
执行的函数是从 ref.current 取的,这个函数每次渲染都会更新,引用着最新的 count。
讲 useRef 的时候说过,ref.current 的值改了不会触发重新渲染,
它就很适合这种保存渲染过程中的一些数据的场景。
再来看一个例子:
js
import { type FC, useState, useRef } from 'react'
const Demo: FC = () => {
const [count, setCount] = useState(0)
const add = () => {
setCount(count + 1)
}
const alertFn = () => {
setTimeout(() => {
alert(count)
}, 3000)
}
return (
<div>
<p>闭包陷阱</p>
<div>
<p>{count}</p>
<button onClick={add}>add</button>
<button onClick={alertFn}>alert</button>
</div>
</div>
)
}
export default Demo
当你先点击alertFn函数,然后快速点击add函数,此时 alert(count) 中的仍然是初始值0,这也是因为闭包导致的。
怎么做呢?仍然是用useRef来解决:
js
import { type FC, useState, useRef } from 'react'
const Demo: FC = () => {
const [count, setCount] = useState(0)
const countRef = useRef(0)
console.log('count改变时都会执行', count)
countRef.current = count
const add = () => {
setCount(count + 1)
}
const alertFn = () => {
setTimeout(() => {
// alert(count)
alert(countRef.current)
}, 3000)
}
return (
<div>
<p>闭包陷阱</p>
<div>
<p>{count}</p>
<button onClick={add}>add</button>
<button onClick={alertFn}>alert</button>
</div>
</div>
)
}
export default Demo
总结
闭包陷阱指的是在 React 组件中,由于闭包特性捕获了当前作用域的 state/prop ,导致在后续操作(如异步函数、定时器)中访问到的仍是旧值,而非最新状态。
原因:
- React 函数组件的执行机制
函数组件每次渲染时,都会生成一个全新的函数作用域,其中的 state、prop 都是该次渲染的 "快照"。
React 的 state 是不可变的,setState(或 setXxx)并不会修改当前 state,而是触发新的渲染并生成新的 state。
- 闭包对作用域的捕获
当在组件中定义回调函数(如事件处理、定时器、异步操作)时,函数会捕获当前渲染作用域中的 state/prop。即使后续状态更新触发了新的渲染,旧的回调函数仍持有对旧作用域中变量的引用。
闭包一般会导致内存泄漏,但是react中异步函数访问旧的 state 本身不会直接导致内存泄漏。这是因为异步函数执行完毕后,若没有其他引用指向这个闭包,它会被垃圾回收机制清理。旧的 state 本身是不可变的快照,即使被闭包引用,只要不再被使用,最终也会被回收。