167 votes

This.setState ne fusionne pas les états comme je m'y attendrais

J'ai l'état suivant:

this.setState({ selected: { id: 1, name: 'Foobar' } });  

Ensuite, je mets à jour l'état:

this.setState({ selected: { name: 'Barfoo' }});

Comme setState est censé fusionner, je m'attendrais à ce que ce soit:

{ selected: { id: 1, name: 'Barfoo' } }; 

Mais au lieu de cela, il supprime l'id et l'état est:

{ selected: { name: 'Barfoo' } }; 

Est-ce un comportement attendu et quelle est la solution pour ne mettre à jour qu'une seule propriété d'un objet d'état imbriqué?

147voto

andreypopp Points 2326

Je pense que setState() ne fait pas de fusion récursive.

Vous pouvez utiliser la valeur de l'état actuel this.state.selected pour construire un nouvel état, puis appeler setState() dessus :

var newSelected = _.extend({}, this.state.selected);
newSelected.name = 'Barfoo';
this.setState({ selected: newSelected });

J'ai utilisé la fonction _.extend() (de la bibliothèque underscore.js) ici pour éviter toute modification de la partie existante selected de l'état en créant une copie superficielle de celle-ci.

Une autre solution serait d'écrire setStateRecursively() qui effectue une fusion récursive sur un nouvel état, puis appelle replaceState() avec :

setStateRecursively: function(stateUpdate, callback) {
  var newState = mergeStateRecursively(this.state, stateUpdate);
  this.replaceState(newState, callback);
}

97voto

user3874611 Points 979

Des assistants d'immuabilité ont récemment été ajoutés à React.addons, donc avec cela, vous pouvez maintenant faire quelque chose comme:

var newState = React.addons.update(this.state, {
  selected: {
    name: { $set: 'Barfoo' }
  }
});
this.setState(newState);

Documentation des assistants d'immuabilité.

38voto

bgannonpl Points 471

Comme de nombreuses réponses utilisent l'état actuel comme base pour fusionner de nouvelles données, je voulais souligner que cela peut être problématique. Les modifications d'état sont mises en file d'attente et ne modifient pas immédiatement l'objet d'état d'un composant. Faire référence aux données d'état avant que la file d'attente n'ait été traitée vous donnera donc des données obsolètes qui ne reflètent pas les modifications en attente que vous avez apportées à setState. Selon la documentation :

setState() ne mute pas immédiatement this.state mais crée une transition d'état en attente. Accéder à this.state après avoir appelé cette méthode peut potentiellement renvoyer la valeur existante.

Cela signifie qu'utiliser l'état "actuel" comme référence dans les appels suivants à setState n'est pas fiable. Par exemple :

  1. Premier appel à setState, mise en file d'attente d'un changement d'objet d'état.
  2. Deuxième appel à setState. Votre état utilise des objets imbriqués, donc vous voulez effectuer une fusion. Avant d'appeler setState, vous obtenez l'objet d'état actuel. Cet objet ne reflète pas les modifications en file d'attente apportées lors du premier appel à setState, ci-dessus, car il s'agit toujours de l'état d'origine, qui devrait maintenant être considéré comme "obsolète".
  3. Effectuer la fusion. Le résultat est l'état d'origine "obsolète" plus les nouvelles données que vous venez de définir, les modifications apportées lors du premier appel initial à setState ne sont pas reflétées. Votre appel à setState met en file d'attente ce deuxième changement.
  4. React traite la file d'attente. Le premier appel à setState est traité, mettant à jour l'état. Le deuxième appel à setState est traité, mettant à jour l'état. L'objet de la deuxième setState a maintenant remplacé le premier, et comme les données que vous aviez lors de cet appel étaient obsolètes, les données obsolètes modifiées de ce deuxième appel ont écrasé les modifications apportées lors du premier appel, qui sont perdues.
  5. Lorsque la file d'attente est vide, React détermine s'il faut rendre, etc. À ce stade, vous allez afficher les modifications apportées lors du deuxième appel à setState, et il sera comme si le premier appel à setState n'avait jamais eu lieu.

Si vous avez besoin d'utiliser l'état actuel (par exemple, pour fusionner des données dans un objet imbriqué), setState accepte alternativement une fonction en argument au lieu d'un objet ; la fonction est appelée après toute mise à jour précédente de l'état et passe l'état en argument - de cette manière, vous pouvez effectuer des changements atomiques garantis de respecter les changements précédents.

11voto

Ryan Shillington Points 558

Je ne voulais pas installer une autre bibliothèque, donc voici encore une autre solution.

Au lieu de :

this.setState({ selected: { name: 'Barfoo' }});

Faites plutôt ceci :

var newSelected = Object.assign({}, this.state.selected);
newSelected.name = 'Barfoo';
this.setState({ selected: newSelected });

Ou, grâce à @icc97 dans les commentaires, de manière encore plus succincte mais discutablement moins lisible :

this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });

Aussi, pour être clair, cette réponse ne viole aucune des préoccupations mentionnées par @bgannonpl ci-dessus.

9voto

Martin Dawson Points 3689

Préserver l'état précédent basé sur la réponse de @bgannonpl :

Lodash exemple :

this.setState((previousState) => _.merge({}, previousState, { selected: { name: "Barfood"} }));

Pour vérifier que cela fonctionne correctement, vous pouvez utiliser la fonction de rappel du deuxième paramètre :

this.setState((previousState) => _.merge({}, previousState, { selected: { name: "Barfood"} }), () => alert(this.state.selected));

J'ai utilisé merge parce que extend supprime sinon les autres propriétés.

React Immutabilité exemple :

import update from "react-addons-update";

this.setState((previousState) => update(previousState, {
    selected:
    { 
        name: {$set: "Barfood"}
    }
});

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X