如何解决React Hook闭包问题?

31

import React, { useState } from "react";
import ReactDOM from "react-dom";

function App() {
  const [count, setCount] = useState(0);

  function handleAlertClick() {
    return (setTimeout(() => {
      alert("You clicked on: " + count);
    }, 3000))
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
      <button onClick={handleAlertClick}>Show alert</button>
    </div>
  );
}

我只是想知道这个是否按照我想的那样工作,或者是否有更好的解释!

无论何时调用setState方法,状态都会获得一个新的引用。这意味着原始状态没有新值,而是创建了一个具有新值的新状态。当我们点击第二个按钮时,事件处理程序函数捕获了原始状态的引用。

即使我们多次点击第一个按钮,当警报被显示时,它将显示事件处理程序捕获其引用的状态的值。

这正确吗?


你遇到了什么问题? - Tushar
我只是问一下我理解的是否正确!! 每当调用setState方法时,状态都会获得一个新引用。这意味着原始状态没有新值,而是创建了一个具有新值的新状态。当我们单击第二个按钮时,事件处理程序函数捕获原始状态的引用。即使我们多次单击第一个按钮,当警报显示时,它将显示事件处理程序捕获其引用的状态的值。 - A2ub
你在这里提到的操作有什么问题吗?我没有看到任何问题,点击5次,然后点击第二次,弹出消息提示"已点击5次"。没有问题。 - Dawei
2个回答

44
< p >弹出框显示过时的“count”值的原因是传递给setTimeout的回调引用了被闭包捕获的“count”的过时值。这通常称为“陈旧的闭包”。

在初次渲染时,传递为setTimeout回调的匿名函数将“count”的值捕获为0,当单击“显示警报”按钮时,回调被排队等待执行,但使用的仍是已过时的“count”值。

在上述情况下,解决显示计数器更新值并修复“陈旧的闭包”问题最简单的方法是使用ref。

function App() {
  const [count, setCount] = useState(0);

  const latestValue = useRef(count);

  const handleAlertClick = () => {
    setTimeout(() => {
      alert(`count is: ${latestValue.current}`);
    }, 3000);
  };

  return (
    <div>
      <p>You clicked {count} times</p>
      <button
        onClick={() => {
          setCount(prev => {
            latestValue.current = prev + 1;
            return prev + 1;
          });
        }}
      >
        Click me
      </button>
      <button onClick={handleAlertClick}>Show alert</button>
    </div>
  );
}

codesandbox 上有工作演示。

钩子(Hooks)在工作时严重依赖闭包,因此很可能会遇到陈旧的闭包(stale-closures)问题。这里有一篇不错的文章,介绍了在使用React Hooks 时如何解决陈旧闭包问题,并演示了在某些情况下如何解决其中一些问题。


请注意,useRef 创建的对象将在组件的整个生命周期内持久存在,并且 React 将在每次渲染时为您提供相同的 ref 对象。这是因为通过引用内存中的同一对象,无论定义或执行回调的作用域如何,都指向相同的对象,从而消除了陈旧的引用。 - jhovanec
@A2ub 我认为你的假设似乎是正确的。你找到答案了吗?我非常想知道。 - Shawn
1
@A2ub 是的。你是正确的。这是因为React使用不可变状态的方法。对于不可变状态,count变量的每个新值都是一个全新的对象(一个新的引用),闭包与它们的词法作用域引用捆绑在一起。因此,在React中,如果您从闭包引用状态变量,则会得到过时的闭包。UseRef可以解决这个问题,因为UseRef提供了一个在组件生命周期内保持一致且可变的引用。使用依赖项数组也可以工作,因为它会在每次状态更改时重新运行effect闭包。 - Daniel Bingham
@subashMahapatra 我很想给你点个踩,因为你在回答和链接的文章中都没有真正回答问题。当人们来到这里时,他们大多数都是想知道如何解决问题,而你却只是展示了如何绕过问题,但并没有解释为什么React的行为与标准JavaScript闭包不同。在你文章中链接的第一个例子中,人们会期望count的行为像value一样(因此不会过时),而不是像message一样。它的行为类似于message,是因为React使用了不可变状态。我不会点踩,但我建议你修改一下。 - Daniel Bingham
实际上,没有使用useRef,什么都不做,原始版本就可以正常工作。不确定我们在这里尝试解决什么问题https://codesandbox.io/s/cold-monad-8z2pj8 - Dawei
显示剩余4条评论

1

https://dmitripavlutin.com/react-hooks-stale-closures/#32-usestate

  function App() {
  const [count, setCount] = useState(0);

  const handleAlertClick = () => {
    setTimeout(() => {
      alert(`count is: ${count}`);
    }, 3000);
  };

  return (
    <div>
      <p>You clicked {count} times</p>
      <button
        onClick={() => {
          setCount((count) => count + 1);
        }}
      >
        Click me
      </button>
      <button onClick={handleAlertClick}>Show alert</button>
    </div>
  );
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接