Il settatore hook useState sovrascrive erroneamente lo stato


31

Ecco il problema: sto cercando di chiamare 2 funzioni con un clic sul pulsante. Entrambe le funzioni aggiornano lo stato (sto usando l'hook useState). La prima funzione aggiorna il valore 1 correttamente a "nuovo 1", ma dopo 1s (setTimeout) viene attivata la seconda funzione e cambia il valore 2 in "nuovo 2" MA! Riporta il valore1 su '1'. Perché sta succedendo? Grazie in anticipo!

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;

potresti registrare lo stato all'inizio di changeValue2?
DanStarns

1
Consiglio vivamente di dividere l'oggetto in due chiamate separate useStateo invece di utilizzarlo useReducer.
Jared Smith,

Sì, secondo questo. Basta usare due chiamate per usare State ()
Esben Skov Pedersen,

const [state, ...], e quindi riferendosi ad esso nel setter ... Userà sempre lo stesso stato.
Johannes Kuhn,

Migliore linea d'azione: utilizzare 2 useStatechiamate separate , una per ogni "variabile".
Dima Tisnek,

Risposte:


30

Benvenuti all'inferno della chiusura . Questo problema si verifica perché ogni volta che setStateviene chiamato, stateottiene un nuovo riferimento di memoria, ma le funzioni changeValue1e changeValue2, a causa della chiusura, mantengono il vecchio stateriferimento iniziale .

Una soluzione per garantire setStateda changeValue1e changeValue2ottenere lo stato più recente consiste nell'utilizzare un callback (con lo stato precedente come parametro):

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

  // ...
};

Puoi trovare una discussione più ampia su questo problema di chiusura qui e qui .


Una richiamata con hook useState sembra essere una funzione non documentata , sei sicuro che funzioni?
HMR

@HMR Sì, funziona ed è documentato su un'altra pagina. Dai un'occhiata alla sezione "Aggiornamenti funzionali" qui: reazionejs.org/docs/hooks-reference.html ("Se il nuovo stato viene calcolato utilizzando lo stato precedente, puoi passare una funzione a setState")
Alberto Trindade Tavares

1
@AlbertoTrindadeTavares Sì, stavo guardando anche i documenti, non sono riuscito a trovare nulla. Grazie mille per la risposta!
Bartek,

1
La tua prima soluzione non è solo una "soluzione facile", è il metodo corretto. Il secondo funzionerebbe solo se il componente fosse progettato come un singleton, e anche allora non ne sono sicuro perché lo stato diventa ogni volta un nuovo oggetto.
Scimonster,

1
Grazie @AlbertoTrindadeTavares! Bello
José Salgado il

19

Le tue funzioni dovrebbero essere così:

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

Quindi assicurati di non perdere alcuna proprietà esistente nello stato corrente usando lo stato precedente quando l'azione viene attivata. Inoltre in questo modo eviti di dover gestire le chiusure.


6

Quando changeValue2viene invocato, lo stato iniziale viene mantenuto in modo che lo stato ritorni allo stato iniziale e quindi la value2proprietà venga scritta.

La volta successiva changeValue2viene invocata successivamente, mantiene lo stato {value1: "1", value2: "new 2"}, quindi la value1proprietà viene sovrascritta.

È necessaria una funzione freccia per il setStateparametro.

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

Ciò che sta accadendo è che entrambi changeValue1e changeValue2vedono lo stato dal rendering in cui sono stati creati , quindi quando il componente esegue il rendering per la prima volta queste 2 funzioni vedono:

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

Quando si fa clic sul pulsante, changeValue1viene chiamato per primo e cambia lo stato {value1: "new1", value2: "2"}come previsto.

Ora, dopo 1 secondo, changeValue2viene chiamato, ma questa funzione continua a vedere lo stato iniziale ( {value1; "1", value2: "2"}), quindi quando questa funzione aggiorna lo stato in questo modo:

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

si finisce per vedere: {value1; "1", value2: "new2"}.

fonte

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.