React Hooks闭包陷阱问题及解决方案
React Hooks闭包陷阱问题及解决方案
React Hooks的闭包陷阱是一个常见的问题,尤其是在处理异步逻辑和状态更新时更容易出现。本文通过一个实际案例,详细介绍了闭包陷阱产生的原因,并提供了三种解决方案:使用useRef、函数式更新和useReducer。
问题描述
需求很简单:一个卡片上有多个下载按钮,点击后请求文件地址。大概实现如下:
export default function App() {
const [loadingMap, setLoadingMap] = useState({});
const handleClick = async (key) => {
setLoadingMap({ ...loadingMap, [key]: true });
await delay(3000);
setLoadingMap({ ...loadingMap, [key]: false });
};
return (
<div className="App">
<button onClick={() => handleClick("a")}>
{loadingMap.a ? "下载中" : "下载a"}
</button>
<button onClick={() => handleClick("b")}>
{loadingMap.b ? "下载中" : "下载b"}
</button>
</div>
);
}
测试时发现问题:如果连续点击 a 和 b 按钮,当 b 下载结束时,a 按钮会被重置为”下载中“。这种情况的产生是因为闭包问题,每次渲染都有独立的状态上下文,导致在事件处理器中获取到的是旧的状态值。
解决方案
关键点在于:在执行状态更新时,如何能获取到最新的状态,而不是本次渲染时的状态。
方案一:useRef
useRef
是 React 开发者的老朋友,是解决 Hooks 问题的万金油。它能存储一个指向不变的变量。
代码做如下更改:
const loadingMap = useRef({});
const forceUpdate = useForceUpdate();
const setLoading = (key, value) => {
loadingMap.current[key] = value;
forceUpdate();
};
把 loadingMap
存在 ref.current
里,就永远能找到它了,所有的渲染里,它的指向都是唯一的。唯一的问题是,修改 ref.current
是不会触发重新渲染的,因此需要使用 forceUpdate
强制刷新。
方案二:setState 的函数式更新
函数式更新好,获取状态没烦恼。setState
可以接收一个函数作为参数,即函数式更新,我们把这个函数称为更新器,这种更新方式不会直接触发重新渲染,而是会把更新器推到一个队列里,最后经过计算统一触发一次更新。更新器的参数是上一个计算后该状态的值,可以理解为最新的状态。
代码做如下更改:
const setMap = (key, value) => {
setLoadingMap((v) => ({
...v,
[key]: value,
}));
};
const handleClick = async (key) => {
setMap(key, true);
await delay(3000);
setMap(key, false);
};
函数式更新有如下优点:
- 总是可以拿到最新的状态
- 没有依赖,如果要在
useEffect
中更新状态,但又不想因状态更新触发 effect 时,用函数式更新很合适 - 批量更新时只会触发一次重新渲染,这可以避免不必要的重新渲染
方案三:useReducer
useReducer
的情况跟 ref
类似,它也是将状态的状态管理转移了,得以避免 Hooks 的闭包陷阱。
代码修改如下:
const reducer = (state, action) => {
return { ...state, ...action };
};
const [loadingMap, dispatch] = useReducer(reducer, {});
const handleClick = async (key) => {
dispatch({ [key]: true });
await delay(3000);
dispatch({ [key]: false });
};
某种程度上,reducer 被称为 Hooks 的作弊模式,它可以解耦业务逻辑和更新方法的关系。和函数式更新一样,它也不需要依赖当前渲染上下文的状态,因此很适合在以下场景使用:需要在 useEffect
这类需要传入依赖的 Hooks 中修改状态,但又不想因依赖改变触发 effect 的场景。
不过使用 useReducer
往往意味着更多的代码逻辑(action、reducer、dispatch... )。
总结
Hooks 的闭包陷阱是非常常见的问题,当代码中有【异步逻辑+get/setState】的场景时,需要注意可能产生闭包陷阱。本文的三种方法除了解决闭包陷阱问题,在批量更新、依赖诚实、避免不必要的 effect 等场景下也都提供了解决方案。
最后,希望 useEvent
尽快到来。