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) // 同样不会更新
相关推荐
帧栈1 小时前
开发避坑指南(27):Vue3中高效安全修改列表元素属性的方法
前端·vue.js
max5006001 小时前
基于桥梁三维模型的无人机检测路径规划系统设计与实现
前端·javascript·python·算法·无人机·easyui
excel2 小时前
使用函数式封装绘制科赫雪花(Koch Snowflake)
前端
萌萌哒草头将军2 小时前
Node.js v24.6.0 新功能速览 🚀🚀🚀
前端·javascript·node.js
持久的棒棒君4 小时前
启动electron桌面项目控制台输出中文时乱码解决
前端·javascript·electron
小离a_a5 小时前
使用原生css实现word目录样式,标题后面的...动态长度并始终在标题后方(生成点线)
前端·css
郭优秀的笔记5 小时前
抽奖程序web程序
前端·css·css3
布兰妮甜6 小时前
CSS Houdini 与 React 19 调度器:打造极致流畅的网页体验
前端·css·react.js·houdini
小小愿望6 小时前
ECharts 实战技巧:揭秘 X 轴末项标签 “莫名加粗” 之谜及破解之道
前端·echarts
小小愿望6 小时前
移动端浏览器中设置 100vh 却出现滚动条?
前端·javascript·css