useEffect中监听器中的state问题

430 阅读2分钟

如题,最近在工作中碰到了使用一个问题,这边简化一下相关代码,

import { useEffect, useRef, useState } from "react";

export default function Home(){
  const [count,setCount] = useState(0);
  const btn = useRef(null);

  const increment = () => {
    setCount(count+1);
  };

  useEffect(() => {
    btn.current.addEventListener('click',increment);
    return () => {
      btn.current.removeEventListener('click',increment);
    }
  }, [])

  useEffect(() => {
    console.log('count now', count);
  },[count]);
  
  return (
    <div>
      <h1>{count}</h1>
      <button ref={btn} >加一</button>
    </div>
  )
}

发现,即使反复点击加一,页面显示的代码块也会停留在1

image.png

原因

这是由于在注册事件时函数的作用域就已经被决定了,通过查验作用域能证明这一点

image.png

那么由此可见,监听函数中用到的数据全被闭包保存,所以内部的count值实际上已然被确定了,而这边的setCount更新函数实际上与外界是同一个,这点可以通过查验react源码确定,这样导致的结果便是在触发监听事件时,虽然能通过setCount正确给外界的count赋值,但是由于setCount(count+1)中的count是内部保存的count,导致每次实际执行的是setCount(0+1),于是造成了错误;

解决方法

1.可以通过在依赖数组中添加state值来解决,由于根本原因是count的旧值,所以只需要在每次count更新时更新监听事件就行了

  useEffect(() => {
    btn.current.addEventListener('click',increment);
    return () => {
      btn.current.removeEventListener('click',increment);
    }
  }, [count])

2.由于setCount能够正常使用,而其中其实通过闭包保存了真实count的值,在调用函数时其可以传入一个函数,函数的第一个入参就是真实的count值,所以也可以通过这样更新

 const increment = () => {
    setCount((c) => c + 1);
  };

3.可以使用useRef替代,由于前方问题得原因是创建函数时就确定了作用域,那么只要传入的不是具体数值,而是对象的引用地址,那么在使用的时候实际还是对同一个对象做出的操作,也就能保证数据的一致,useR俄方、就能完成这样的要求

const count = useRef(0);

 const increment = () => {
    count.current += 1;
  };

总结

在useEffect依赖数组为空的情况下,添加监听器或类似setInterval的事件时,要注意内部有无用到外界state的值,由于useEffect特性,依赖数组为空的时候只会在初次渲染后执行,导致外界state更新时其添加的监听器不能得到新值,由此可能导致一系列问题