Установщик ловушки useState неправильно перезаписывает состояние

Здесь проблема: я пытаюсь вызвать 2 функции одним нажатием кнопки. Обе функции обновляют состояние (я использую хук useState). Первая функция корректно обновляет значение 1 до "нового 1", но через 1 с (setTimeout) запускается вторая функция, и она меняет значение 2 до "нового 2", НО! Он устанавливает значение 1 обратно в "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;

Ответы

Ответ 1

Добро пожаловать в аду закрытия. Эта проблема возникает из-за того, что всякий раз, когда вызывается setState, state получает новую ссылку на память, но функции changeValue1 и changeValue2 из-за закрытия сохраняют старую начальную ссылку state.

Решение, обеспечивающее получение setState из changeValue1 и changeValue2 самого последнего состояния, заключается в использовании обратного вызова (имеющего свежее текущее состояние в качестве параметра):

import React, { useState } from "react";

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

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

  // ...
};

Вы можете найти более широкое обсуждение этой проблемы закрытия здесь и здесь.

Ответ 2

Ваши функции должны быть такими:

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

Таким образом, вы гарантируете, что не пропускаете ни одно существующее свойство в текущем состоянии, используя предыдущее состояние при запуске действия. Также, таким образом, вы избегаете необходимости управлять замыканиями.

Ответ 3

Когда вызывается changeValue2, сохраняется исходное состояние, поэтому состояние возвращается в исходное состояние, а затем записывается свойство 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>

Ответ 4

Дело в том, что и changeValue1, и changeValue2 видят состояние от рендеринга, который они были созданы в, поэтому, когда ваш компонент рендерит в первый раз, эти две функции видят:

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"}.

источник