useState闭包陷阱
在React中,使用useState时遇到闭包陷阱通常指的是在函数组件内部创建的函数无法正确访问最新的state值。这通常发生在将useState的更新函数或状态值传递给另一个函数组件或函数时,而这些函数稍后又被调用,但此时的状态已经改变。
闭包陷阱的原因
在 Javascript 中, 闭包是当函数可以记住并访问其此法作用域时发生的现象,即使该函数在其原始作用域之外执行。在React中,当创建一个函数并将其传递个子组件或事件处理器,然后在某个时间点再次调用这个函数时,如果该函数闭包了useState的状态或更新函数,它将捕获到的是创建时的状态和更新函数,而不是调用时的状态和更新函数
举例
举个简单例子
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?
答案:不会,打印会一直是0, 而显示在dom上会是最初显示0,而后一直是1
由于useEffect依赖数组为空,也就是只会执行并保留第一次的setup函数, 而该函数中引用了当时的count变量,形成了闭包, 所以实际的执行效果如下:
第一次运行:count = 0 > setCount(0 + 1) > 页面显示1
第二次运行:count = 0 > setCount(0 + 1) > 页面显示1
解决方案
- 01
setState的另外一种使用方式, 支持函数作为参数,其函数的参数为当前旧值,通过旧值更新去保证整个state的逻辑。由于使用了旧值参数的count,也就没有使用count, 避免形成闭包,每次的 count 都是参数传入的上一次的 state
import { useEffect, useState } from 'react';
function App() {
const [count,setCount] = useState(0);
useEffect(() => {
setInterval(() => {
console.log(count);
// TODO setCount支持函数作为入参
setCount(oldCount => oldCount + 1);
}, 1000);
}, []);
return <div>{count}</div>
}
export default App;
- 02
useEffect增加依赖监听,比如这个案例中,可以理解是需要跟进count的变化,然后再进行+1,于是把count增加effect的依赖, 如下代码,那么原来的setInterval不能再用,因为setup会持续多次执行,按照这里例子,把setInterval换成setTimeout刚好能覆盖此场景,首次运行setup执行,开启定时器1s后 setCount进行更新值,当count变化后,重新运行setup, 以此循环, 其实也可以保留setInterval 但是需要cleanup, 每次运行前清除上一次的定时器
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
console.log(count);
setCount((count) => count + 1);
}, 1000);
}, [count]);
return <div>{count}</div>;
}
export default App;
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;
- 03 另外还有一种使用
useRef,此方式这里是参考到了关于React通关小册里面提交到的,但不太建议,思想上不太接受。
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的值为最新函数,那么updateCount里面的count则引用的都是每次的最新值,则保证了更新的准确性。在useEffect中,setup在这里只运行一次,保证setInterval不会重置,执行的函数从ref.current取到,保证count最新状态。
这个地方有点绕,不太好理解,比如取消ref.current = updateCount; 这句话后则效果直接不行, 大概是因为没有每次渲染后重置一个新的updateCount函数,则每次updateCount函数的count还是闭包的旧变量。平时不太好注意,建议平时还是少用此方式。
setState常用使用问题注意
- 异步状态更新
const increate = () => {
setCount(count + 1)
console.log(count) // 还是旧值
}
- 直接操作变更对象或者数组
const [arr, setArr] = useState([])
arr.push(1) // 直接push已经直接变更了arr数据, setState过程判断值没变化不会更新页面
setArr(arr)
const [obj, setObj] = useState({})
obj.a = 1
setObj(obj) // 同样不会更新