useState hook setter écrase incorrectement l'état

31

Voici le problème: j'essaie d'appeler 2 fonctions en cliquant sur un bouton. Les deux fonctions mettent à jour l'état (j'utilise le hook useState). La première fonction met correctement à jour value1 en 'new 1', mais après 1s (setTimeout) la deuxième fonction se déclenche, et elle change la valeur 2 en 'new 2' MAIS! Il a remis la valeur1 à «1». Pourquoi cela arrive-t-il? Merci d'avance!

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
la source
pourriez-vous enregistrer l'état au début de changeValue2?
DanStarns
1
Je vous recommande fortement de diviser l'objet en deux appels distincts useStateou de l'utiliser à la place useReducer.
Jared Smith
Oui - seconde ceci. Utilisez simplement deux appels pour utiliserState ()
Esben Skov Pedersen
const [state, ...], puis en y faisant référence dans le setter ... Il utilisera le même état tout le temps.
Johannes Kuhn
Meilleur plan d'action: utilisez 2 useStateappels distincts , un pour chaque "variable".
Dima Tisnek

Réponses:

30

Bienvenue dans l'enfer de la fermeture . Ce problème se produit car chaque fois qu'il setStateest appelé, stateobtient une nouvelle référence de mémoire, mais les fonctions changeValue1et changeValue2, en raison de la fermeture, conservent l'ancienne stateréférence initiale .

Une solution pour garantir le setStatefrom changeValue1et changeValue2obtenir le dernier état consiste à utiliser un rappel (ayant l'état précédent comme paramètre):

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

  // ...
};

Vous pouvez trouver une discussion plus large sur ce problème de fermeture ici et ici .

Alberto Trindade Tavares
la source
Un rappel avec le crochet useState semble être une fonctionnalité non documentée, êtes-vous sûr que cela fonctionne?
HMR
@HMR Oui, cela fonctionne et c'est documenté sur une autre page. Jetez un œil à la section " Mises à jour fonctionnelles" ici: reactjs.org/docs/hooks-reference.html ("Si le nouvel état est calculé en utilisant l'état précédent, vous pouvez passer une fonction à setState")
Alberto Trindade Tavares
1
@AlbertoTrindadeTavares Oui, je regardais également les documents, je n'ai rien trouvé. Merci beaucoup pour la réponse!
Bartek
1
Votre première solution n'est pas seulement une «solution facile», c'est la bonne méthode. Le second ne fonctionnerait que si le composant est conçu comme un singleton, et même alors, je ne suis pas sûr de cela car l'état devient à chaque fois un nouvel objet.
Scimonster
1
Merci @AlbertoTrindadeTavares! Nice one
José Salgado
19

Vos fonctions devraient être comme ceci:

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

Ainsi, vous vous assurez de ne manquer aucune propriété existante dans l'état actuel en utilisant l'état précédent lorsque l'action est déclenchée. Ainsi vous évitez ainsi d'avoir à gérer les fermetures.

Dez
la source
6

Lorsque changeValue2est invoqué, l'état initial est maintenu afin que l'état revienne à l'état initial et que la value2propriété soit écrite.

La prochaine fois changeValue2est invoquée après cela, elle détient l'état {value1: "1", value2: "new 2"}, donc la value1propriété est écrasée.

Vous avez besoin d'une fonction de flèche pour le setStateparamètre.

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
la source
3

Ce qui se passe, c'est que les deux changeValue1et changeValue2voient l'état du rendu dans lequel ils ont été créés , donc quand votre composant rend pour la première fois ces 2 fonctions voient:

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

Lorsque vous cliquez sur le bouton, changeValue1est appelé en premier et change l'état {value1: "new1", value2: "2"}comme prévu.

Maintenant, après 1 seconde, changeValue2est appelée, mais cette fonction voit toujours l'état initial ( {value1; "1", value2: "2"}), donc quand cette fonction met à jour l'état de cette façon:

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

on finit par voir: {value1; "1", value2: "new2"}.

la source

El Aoutar Hamza
la source