Search K
Appearance
Appearance
原文转载地址:
前几天面试被问到react
中的闭包陷阱,被面试官一顿吊打,特此记录下来
React
的闭包陷阱是指在使用 React Hooks
时,由于闭包特性导致在某些函数或异步操作中无法正确访问到更新后状态或 prop
的值,而仍旧使用了旧值。下面通过几个代码示例来具体说明闭包陷阱的几种常见情形:
/**
* useState闭包陷阱
*/
import React from 'react'
import { useState } from 'react'
function App01() {
const [count, setCount] = useState(0)
const handleClick = () => {
setTimeout(() => {
console.log('Count inside setTimeout:', count); // 这里打印的一直是上一个旧值,形成了闭包陷阱
}, 1000)
setCount(count + 1)
}
return (
<div>
<p>Current Count:{count}</p>
<button onClick={() => { handleClick() }}>Increment</button>
</div>
)
}
export default App01
在这个例子中,handleClick
函数内的 setTimeout
回调形成了一个闭包,它捕获了首次渲染时 count
的值(即 0
)。当用户点击按钮触发 handleClick
时,虽然 setCount
更新了状态,但 setTimeout
回调内的 count
仍指向最初捕获的 0
,因此在延时一秒后打印出的 count
值不是预期的更新值
下面是一个关于react
父子组件传值的闭包陷阱示例
Child.jsx
import React, {useState} from 'react'
function Child(props) {
const [data, setData] = useState(0)
const sendDataToFather = () => {
setData(data + 1)
props.onSharedDataChange(data)
}
return (
<div>
<div>子组件中的data: { data }</div>
<button onClick={ () => { sendDataToFather() } }>Send Data To Father</button>
</div>
)
}
export default Child
Parent.jsx
import React, {useState} from 'react'
import Child from "./Child";
function Parent() {
const [shareData, setShareData] = useState(0)
const onSharedDataChange = (data) => {
setShareData(data)
}
return (
<div>
<div>
父组件中的data: {shareData}
</div>
<div>
<Child onSharedDataChange={onSharedDataChange}/>
</div>
</div>
)
}
export default Parent
预期结果应该是父组件和子组件中的data
始终保持一致,但是可以打先父组件中的data
始终是上一次更新的旧值
解决方法,使用useEffect
监听依赖: Child.jsx
import React, { useState, useEffect } from 'react'
function Child(props) {
const [data, setData] = useState(0)
const sendDataToFather = () => {
setData(data + 1)
props.onSharedDataChange(data)
}
// 设置一个依赖收集的副作用函数,数据变化后重新通信传值
useEffect(() => {
props.onSharedDataChange(data)
}, [data])
return (
<div>
<div>子组件中的data: {data}</div>
<button onClick={() => { sendDataToFather() }}>Send Data To Father</button>
</div>
)
}
export default Child
import React, { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // 闭包陷阱:这里的 count 始终是初始值 0
}, 1000);
return () => clearInterval(intervalId);
}, []);
const handleClick = () => {
console.log(`Current count: ${count}`);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Show Count</button>
</div>
);
}
export default Counter;
在这个示例中,setInterval
回调函数中的 count
始终是初始值 0,因为这个回调函数是在组件第一次渲染时创建的,因此它捕获了 count
的初始值。解决这个问题的方法是使用函数形式的 setState:
解决办法:
useEffect(() => {
const intervalId = setInterval(() => {
setCount(prevCount => prevCount + 1); // 使用函数形式的 setState
}, 1000);
return () => clearInterval(intervalId);
}, []);
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),导致界面展示的是错误的用户信息。
解决方法:
正确列出 useEffect
的依赖
useEffect(() => {
// ...
}, [userId]);
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
实例,导致过滤功能失效。
解决方案:
将 setFilterText
添加到 useCallback
的依赖列表:
const handleFilterChange = useCallback(
event => {
setFilterText(event.target.value);
},
[setFilterText]
);