useState hook setter sobrescribe incorrectamente el estado

31

Aquí está el problema: estoy tratando de llamar a 2 funciones con un clic de botón. Ambas funciones actualizan el estado (estoy usando el enlace useState). La primera función actualiza el valor1 correctamente a 'nuevo 1', pero después de 1s (setTimeout) se dispara la segunda función, y cambia el valor 2 a 'nuevo 2' ¡PERO! Establece el valor1 de nuevo a '1'. ¿Por qué está pasando esto? ¡Gracias por adelantado!

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
fuente
¿podría registrar el estado al comienzo de changeValue2?
DanStarns
1
Le recomiendo que divida el objeto en dos llamadas separadas para useStateusar o en su lugar useReducer.
Jared Smith
Sí, segundo esto. Solo use dos llamadas para usar State ()
Esben Skov Pedersen
const [state, ...], y luego haciendo referencia a él en el setter ... Utilizará el mismo estado todo el tiempo.
Johannes Kuhn el
El mejor curso de acción: use 2 useStatellamadas separadas , una para cada "variable".
Dima Tisnek

Respuestas:

30

Bienvenido al infierno de cierre . Este problema ocurre porque cada vez que setStatese llama, stateobtiene una nueva referencia de memoria, pero las funciones changeValue1y changeValue2, debido al cierre, mantienen la statereferencia inicial anterior .

Una solución para garantizar la setStatede changeValue1y changeValue2recupera el estado más reciente es el uso de una devolución de llamada (que tiene el estado anterior como un parámetro):

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" }));
  };

  // ...
};

Puede encontrar una discusión más amplia sobre este tema de cierre aquí y aquí .

Alberto Trindade Tavares
fuente
Una devolución de llamada con useState hook parece ser una característica no documentada , ¿estás seguro de que funciona?
HMR
@HMR Sí, funciona y está documentado en otra página. Eche un vistazo a la sección "Actualizaciones funcionales" aquí: reactjs.org/docs/hooks-reference.html ("Si el nuevo estado se calcula utilizando el estado anterior, puede pasar una función a setState")
Alberto Trindade Tavares
1
@AlbertoTrindadeTavares Sí, también estaba mirando los documentos, no pude encontrar nada. ¡Muchas gracias por la respuesta!
Bartek
1
Su primera solución no es solo una "solución fácil", es el método correcto. El segundo solo funcionaría si el componente está diseñado como un singleton, e incluso entonces no estoy seguro de eso porque el estado se convierte en un nuevo objeto cada vez.
Scimonster
1
¡Gracias @AlbertoTrindadeTavares! Nice one
José Salgado
19

Sus funciones deberían ser así:

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

Por lo tanto, se asegura de no perder ninguna propiedad existente en el estado actual utilizando el estado anterior cuando se activa la acción. También así evitas tener que gestionar cierres.

Dez
fuente
6

Cuando changeValue2se invoca, el estado inicial se mantiene para que el estado vuelva al estado inicial y luego value2se escribe la propiedad.

La próxima vez que changeValue2se invoca después de eso, mantiene el estado {value1: "1", value2: "new 2"}, por lo que value1se sobrescribe la propiedad.

Necesita una función de flecha para el setStateparámetro.

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
fuente
3

Lo que pasa es que tanto changeValue1y changeValue2ver el estado del render que fueron creados en , por lo que cuando su componente rinde por primera vez estas 2 funciones, véase:

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

Cuando hace clic en el botón, changeValue1se llama primero y cambia el estado a {value1: "new1", value2: "2"}lo esperado.

Ahora, después de 1 segundo, changeValue2se llama, pero esta función aún ve el estado inicial ( {value1; "1", value2: "2"}), por lo que cuando esta función actualiza el estado de esta manera:

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

terminas viendo: {value1; "1", value2: "new2"}.

fuente

El Aoutar Hamza
fuente