useState Hook的闭包陷阱及使用注意

useState闭包陷阱

React中,使用useState时遇到闭包陷阱通常指的是在函数组件内部创建的函数无法正确访问最新的state值。这通常发生在将useState的更新函数或状态值传递给另一个函数组件或函数时,而这些函数稍后又被调用,但此时的状态已经改变。

闭包陷阱的原因

Javascript 中, 闭包是当函数可以记住并访问其此法作用域时发生的现象,即使该函数在其原始作用域之外执行。在React 中,当创建一个函数并将其传递个子组件或事件处理器,然后在某个时间点再次调用这个函数时,如果该函数闭包了useState状态 或更新函数,它将捕获到的是创建时的状态和更新函数,而不是调用时的状态和更新函数

举例

举个简单例子

javascript 复制代码
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
javascript 复制代码
	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, 每次运行前清除上一次的定时器
javascript 复制代码
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;
javascript 复制代码
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通关小册里面提交到的,但不太建议,思想上不太接受。
javascript 复制代码
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常用使用问题注意
  • 异步状态更新
javascript 复制代码
const increate = () => {
	setCount(count + 1)
	console.log(count) // 还是旧值
}
  • 直接操作变更对象或者数组
javascript 复制代码
const [arr, setArr] = useState([])
arr.push(1) // 直接push已经直接变更了arr数据, setState过程判断值没变化不会更新页面
setArr(arr)

const [obj, setObj] = useState({})
obj.a = 1
setObj(obj) // 同样不会更新
相关推荐
遇到困难睡大觉哈哈4 分钟前
CSS中的Element语法
前端·css
Real_man10 分钟前
新物种与新法则:AI重塑开发与产品未来
前端·后端·面试
小彭努力中11 分钟前
147.在 Vue3 中使用 OpenLayers 地图上 ECharts 模拟飞机循环飞行
前端·javascript·vue.js·ecmascript·echarts
老马聊技术13 分钟前
日历插件-FullCalendar的详细使用
前端·javascript
咔咔一顿操作15 分钟前
Cesium实战:交互式多边形绘制与编辑功能完全指南(最终修复版)
前端·javascript·3d·vue
LuckyLay2 小时前
使用 Docker 搭建 Rust Web 应用开发环境——AI教你学Docker
前端·docker·rust
pobu1682 小时前
aksk前端签名实现
java·前端·javascript
烛阴2 小时前
带参数的Python装饰器原来这么简单,5分钟彻底掌握!
前端·python
0wioiw02 小时前
Flutter基础(前端教程⑤-组件重叠)
开发语言·前端·javascript
冰天糖葫芦2 小时前
VUE实现数字翻牌效果
前端·javascript·vue.js