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.