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

31

Вот проблема: я пытаюсь вызвать 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;
Bartek
источник
не могли бы вы записать состояние в начале changeValue2?
DanStarns
1
Я настоятельно рекомендую либо разделить объект на два отдельных вызова, useStateлибо использовать вместо него useReducer.
Джаред Смит,
Да - во вторых это. Просто используйте два вызова для использования State ()
Эсбен Сков Педерсен
const [state, ...]и затем ссылаясь на него в установщике ... Он будет использовать одно и то же состояние все время.
Йоханнес Кун
Лучший способ действий: использовать 2 отдельных useStateвызова, по одному для каждой «переменной».
Дима Тиснек

Ответы:

30

Добро пожаловать в закрытие ада . Эта проблема возникает потому, что всякий раз, когда 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((prevState) => ({ ...prevState, value1: "new 1" }));
  };
  const changeValue2 = () => {
    setState((prevState) => ({ ...prevState, value2: "new 2" }));
  };

  // ...
};

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

Альберто Триндаде Таварес
источник
Обратный вызов с перехватом useState кажется недокументированной функцией, вы уверены, что это работает?
HMR
@HMR Да, это работает, и это задокументировано на другой странице. Взгляните на раздел «Функциональные обновления» здесь: Reactionjs.org/docs/hooks-reference.html («Если новое состояние вычисляется с использованием предыдущего состояния, вы можете передать функцию setState»)
Альберто Триндаде Таварес
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вызывается, начальное состояние сохраняется, поэтому состояние возвращается в исходное состояние, а затем 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>

ZMag
источник
3

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

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

источник

Эль Аутар Хамза
источник