React 的闭包陷阱是指在使用 React Hooks 时,由于闭包特性导致在某些函数或异步操作中无法正确访问到更新后状态或 prop 的值,而仍旧使用了旧值。下面通过几个代码示例来具体说明闭包陷阱的几种常见情形:
示例 1: useState
闭包陷阱
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setTimeout(() => {
console.log('Count inside setTimeout:', count); // 问题:总是显示初始值 0,而非实际点击后的值
}, 1000);
setCount(count + 1);
}
return (
<div>
<p>Current Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
在这个例子中,handleClick
函数内的 setTimeout
回调形成了一个闭包,它捕获了首次渲染时 count
的值(即 0
)。当用户点击按钮触发 handleClick
时,虽然 setCount
更新了状态,但 setTimeout
回调内的 count
仍指向最初捕获的 0
,因此在延时一秒后打印出的 count
值不是预期的更新值。
下面是一个关于useState的闭包陷阱
当子组件值变换后给父组件传值,发现传过来的值任然是跟新之前的值,
子:
import React, { useState, useEffect } from "react";
const BrotherComponentA = (props) => {
const [dataB, setDataB] = useState(0);
// useEffect(() => {
// props.onSharedDataChange(dataB);
// }, [dataB]);
const sendDataToBrotherB = () => {
setDataB((dataB) => dataB + 1); // 修改兄弟组件的数据
props.onSharedDataChange(dataB);
};
return (
<div>
{dataB}
<button onClick={sendDataToBrotherB}>Send Data to Brother B</button>
</div>
);
};
export default BrotherComponentA;
父:
import React, { useState } from 'react';
import BrotherComponentA from './Bpp';
const ParentComponent = () => {
const [sharedData, setSharedData] = useState(null);
const handleDataChange = (data) => {
console.log("Received data from BrotherComponentA:", data)
setSharedData(data);
}
return (
<div>
<BrotherComponentA onSharedDataChange={handleDataChange} />
{sharedData}
</div>
);
}
export default ParentComponent;
解决办法:
useEffect(() => {
props.onSharedDataChange(dataB);
}, [dataB]);
注释:props.onSharedDataChange(dataB);
使用useEffect去依赖收集跟新
示例 2: useEffect
闭包陷阱
import { useState, useEffect } from 'react';
function FetchUser() {
const [userId, setUserId] = useState(1);
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`https://api.example.com/users/${userId}`)
.then(response => response.json())
.then(data => setUser(data));
}, []); // 问题:遗漏了 `userId` 作为依赖
function handleUserIdChange(newId) {
setUserId(newId);
}
return (
<>
<input type="number" value={userId} onChange={e => handleUserIdChange(e.target.value)} />
{user && <p>User Name: {user.name}</p>}
</>
);
}
此处 useEffect
用于获取指定 userId
的用户信息。然而,useEffect
的依赖数组为空,意味着它仅在组件挂载时执行一次。当 handleUserIdChange
调用 setUserId
更新 userId
时,useEffect
不会重新执行,因为它没有将 userId
列为依赖。结果,尽管用户输入了新的 ID,fetch 请求仍使用了初始的 userId
值(即 1
),导致界面展示的是错误的用户信息。
示例 3: useCallback
闭包陷阱
import { useState, useCallback } from 'react';
function FilteredList({ items }) {
const [filterText, setFilterText] = useState('');
const filteredItems = items.filter(item => item.includes(filterText));
const handleFilterChange = useCallback(
event => {
setFilterText(event.target.value);
},
[] // 问题:遗漏了 `setFilterText` 作为依赖
);
return (
<div>
<input type="text" value={filterText} onChange={handleFilterChange} />
<ul>
{filteredItems.map(item => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
这里 useCallback
用于缓存 handleFilterChange
函数以避免不必要的重渲染。然而,依赖数组为空,意味着 handleFilterChange
在组件整个生命周期内都不会改变。当 setFilterText
被外部因素(如热重载)替换时,handleFilterChange
仍然引用着旧的 setFilterText
实例,导致过滤功能失效。
解决方案
对于上述示例中的闭包陷阱,相应的解决办法如下:
-
示例 1: 使用函数式更新方式,传递一个函数给
setCount
:setCount(prevCount => prevCount + 1);
-
示例 2: 正确列出
useEffect
的依赖:useEffect(() => { // ... }, [userId]);
-
示例 3: 将
setFilterText
添加到useCallback
的依赖列表:const handleFilterChange = useCallback( event => { setFilterText(event.target.value); }, [setFilterText] );
通过正确处理闭包和依赖关系,可以避免 React Hooks 中的闭包陷阱,确保状态和副作用逻辑按预期工作。