useState挂钩设置器错误地覆盖了状态


31

问题出在这里:我试图在单击按钮时调用2个函数。这两个函数都会更新状态(我正在使用useState挂钩)。第一个函数将value1正确更新为'new 1',但是在1s(setTimeout)之后触发,第二个函数将值2更改为'new 2',但是!它将value1设置回'1'。为什么会这样呢?提前致谢!

import React, { useState } from "react";

const Test = () => {
  const [state, setState] = useState({
    value1: "1",
    value2: "2"
  });

  const changeValue1 = () => {
    setState({ ...state, value1: "new 1" });
  };
  const changeValue2 = () => {
    setState({ ...state, value2: "new 2" });
  };

  return (
    <>
      <button
        onClick={() => {
          changeValue1();
          setTimeout(changeValue2, 1000);
        }}
      >
        CHANGE BOTH
      </button>
      <h1>{state.value1}</h1>
      <h1>{state.value2}</h1>
    </>
  );
};

export default Test;

您可以在开始时记录状态changeValue2吗?
DanStarns

1
我强烈建议您将对象拆分为两个单独的调用,useState或者改为使用useReducer
贾里德·史密斯

是的-第二个。只需使用两次调用useState()
Esben斯科夫佩德森

const [state, ...],然后在设置器中引用它...它将始终使用相同的状态。
约翰内斯·库恩

最佳做法:使用2个单独的useState调用,每个“变量”调用一个。
Dima Tisnek '19

Answers:


30

欢迎来到封闭地狱。发生此问题的原因setState是,无论何时调用,state都会获取新的内存引用,但是由于闭包,函数changeValue1和会changeValue2保留旧的初始state引用。

确保setState从源changeValue1changeValue2获取最新状态的解决方案是使用回调(将先前状态作为参数):

import React, { useState } from "react";

const Test = () => {
  const [state, setState] = useState({
    value1: "1",
    value2: "2"
  });

  const changeValue1 = () => {
    setState((prevState) => ({ ...prevState, value1: "new 1" }));
  };
  const changeValue2 = () => {
    setState((prevState) => ({ ...prevState, value2: "new 2" }));
  };

  // ...
};

您可以在此处此处找到有关此关闭问题的更广泛讨论。


带有useState钩子的回调似乎是一个未记录的功能,您确定可行吗?
HMR

@HMR是的,它可以工作,并在另一页上进行了记录。在这里查看“功能更新”部分:reactjs.org/docs/hooks-reference.html(“如果使用先前状态计算新状态,则可以将函数传递给setState”)
Alberto Trindade Tavares

1
@AlbertoTrindadeTavares是的,我也在看文档,找不到任何东西。非常感谢您的回答!
Bartek

1
您的第一个解决方案不只是“简单的解决方案”,而是正确的方法。第二个仅在组件设计为单例的情况下才有效,即使那样我也不确定,因为状态每次都会变成一个新对象。
Scimonster

1
谢谢@AlbertoTrindadeTavares!尼斯一个
何塞·萨尔加多

19

您的功能应如下所示:

const changeValue1 = () => {
    setState((prevState) => ({ ...prevState, value1: "new 1" }));
};
const changeValue2 = () => {
    setState((prevState) => ({ ...prevState, value2: "new 2" }));
};

因此,通过在触发操作时使用以前的状态,可以确保您不会丢失当前状态下的任何现有属性。这样也避免了必须管理闭包。


6

changeValue2被调用时,初始状态保持如此状态转变回inital状态,然后value2财产被写入。

changeValue2此后下次调用时,它将保持状态{value1: "1", value2: "new 2"},因此value1属性被覆盖。

您需要使用箭头功能作为setState参数。

const Test = () => {
  const [state, setState] = React.useState({
    value1: "1",
    value2: "2"
  });

  const changeValue1 = () => {
    setState(prev => ({ ...prev, value1: "new 1" }));
  };
  const changeValue2 = () => {
    setState(prev => ({ ...prev, value2: "new 2" }));
  };

  return (
    <React.Fragment>
      <button
        onClick={() => {
          changeValue1();
          setTimeout(changeValue2, 1000);
        }}
      >
        CHANGE BOTH
      </button>
      <h1>{state.value1}</h1>
      <h1>{state.value2}</h1>
    </React.Fragment>
  );
};

ReactDOM.render(<Test />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>


3

发生了什么事是,无论changeValue1changeValue2看到国家从他们创建的渲染,所以当你的组件渲染的第一次这两个功能上看到:

state= {
  value1: "1",
  value2: "2"
}

当您单击该按钮时,changeValue1将首先被调用并将状态更改{value1: "new1", value2: "2"}为预期的状态。

现在,在1秒后被changeValue2调用,但是此函数仍然看到初始状态({value1; "1", value2: "2"}),因此当此函数以这种方式更新状态时:

setState({ ...state, value2: "new 2" });

您最终看到:{value1; "1", value2: "new2"}

资源

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.