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) // 同样不会更新
相关推荐
石小石Orz9 分钟前
如何将本地文件转成流数据传递给后端?
前端·vue.js
Codebee1 小时前
OneCode核心概念解析——View(视图)
前端·人工智能
GIS之路1 小时前
GIS 数据质检:验证 Geometry 有效性
前端
GIS之路1 小时前
GeoJSON 数据简介
前端
今阳1 小时前
鸿蒙开发笔记-16-应用间跳转
android·前端·harmonyos
前端小饭桌1 小时前
CSS属性值太多记不住?一招教你搞定
前端·css
快起来别睡了1 小时前
深入浏览器底层原理:从输入URL到页面显示全过程解析
前端·架构
阿星做前端1 小时前
一个倒计时功能引发的线上故障
前端·javascript·react.js
莯炗1 小时前
CSS知识补充 --- 控制继承
前端·css·css继承·css控制继承
tianzhiyi1989sq2 小时前
Vue框架深度解析:从Vue2到Vue3的技术演进与实践指南
前端·javascript·vue.js