this.setState ne fusionne pas les états comme je m'y attendais

160

J'ai l'état suivant:

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

Ensuite, je mets à jour l'état:

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

Puisqu'il setStateest supposé fusionner, je m'attendrais à ce que ce soit:

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

Mais à la place, il mange l'identifiant et l'état est:

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

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

Pickels
la source

Réponses:

143

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

Vous pouvez utiliser la valeur de l'état actuel this.state.selectedpour construire un nouvel état, puis appeler setState()celui-ci:

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

J'ai utilisé la fonction _.extend()function (de la bibliothèque underscore.js) ici pour empêcher la modification de la selectedpartie existante de l'état en créant une copie superficielle de celui-ci.

Une autre solution serait d'écrire setStateRecursively()ce qui fusionne récursivement sur un nouvel état et appelle ensuite replaceState()avec:

setStateRecursively: function(stateUpdate, callback) {
  var newState = mergeStateRecursively(this.state, stateUpdate);
  this.replaceState(newState, callback);
}
andreypopp
la source
7
Cela fonctionne-t-il de manière fiable si vous le faites plus d'une fois ou y a-t-il une chance que react mettra en file d'attente les appels replaceState et le dernier gagnera?
edoloughlin
111
Pour les personnes qui préfèrent utiliser extend et qui utilisent également babel, vous pouvez faire this.setState({ selected: { ...this.state.selected, name: 'barfoo' } })ce qui est traduit enthis.setState({ selected: _extends({}, this.state.selected, { name: 'barfoo' }) });
Pickels
8
@Pickels c'est génial, joliment idiomatique pour React et ne nécessite pas underscore/ lodash. Faites-en sa propre réponse, s'il vous plaît.
ericsoco
14
this.state n'est pas nécessairement à jour . Vous pouvez obtenir des résultats inattendus en utilisant la méthode extend. Passer une fonction de mise à jour à setStateest la bonne solution.
Strom
1
@ EdwardD'Souza C'est la bibliothèque lodash. Il est couramment appelé comme un trait de soulignement, de la même manière que jQuery est couramment appelé comme dollarign. (Donc, c'est _.extend ().)
mjk
96

Des assistants d'immuabilité ont été récemment 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 aides d'immuabilité .

user3874611
la source
7
la mise à jour est obsolète. facebook.github.io/react/docs/update.html
thedanotto
3
@thedanotto Il semble que la React.addons.update()méthode soit obsolète, mais la bibliothèque de remplacement github.com/kolodny/immutability-helper contient une update()fonction équivalente .
Bungle
37

Étant donné que de nombreuses réponses utilisent l'état actuel comme base pour fusionner de nouvelles données, je voulais souligner que cela peut casser. Les changements d'état sont mis en file d'attente et ne modifient pas immédiatement l'objet d'état d'un composant. Le fait de référencer les données d'état avant le traitement de la file d'attente vous donnera donc des données périmées qui ne reflètent pas les modifications en attente que vous avez apportées dans setState. À partir de la documentation:

setState () ne modifie 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 que l'utilisation de 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 objet de changement d'état
  2. Deuxième appel à setState. Votre état utilise des objets imbriqués, vous souhaitez donc 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 effectué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 «périmé».
  3. Effectuer la fusion. Le résultat est l'état «périmé» d'origine plus les nouvelles données que vous venez de définir, les modifications de l'appel initial setState ne sont pas reflétées. Votre appel setState met en file d'attente ce deuxième changement.
  4. File d'attente des processus de réaction. Le premier appel setState est traité, mise à jour de l'état. Le deuxième appel setState est traité, mise à jour de l'état. Le deuxième objet setState a maintenant remplacé le premier, et comme les données que vous aviez lors de cet appel étaient périmées, les données périmées modifiées de ce deuxième appel ont écrasé les modifications apportées au premier appel, qui sont perdues.
  5. Lorsque la file d'attente est vide, React détermine s'il faut effectuer le rendu, etc. À ce stade, vous rendrez les modifications apportées au deuxième appel à setState, et ce sera comme si le premier appel à setState ne s'était jamais produit.

Si vous devez utiliser l'état actuel (par exemple pour fusionner des données dans un objet imbriqué), setState accepte alternativement une fonction comme argument au lieu d'un objet; la fonction est appelée après toutes les mises à jour précédentes de state, et transmet l'état en tant qu'argument - donc cela peut être utilisé pour apporter des modifications atomiques garanties de respecter les modifications précédentes.

bgannonpl
la source
5
Existe-t-il un exemple ou plusieurs documents expliquant comment procéder? afaik, personne n'en tient compte.
Josef.B
@Dennis Essayez ma réponse ci-dessous.
Martin Dawson
Je ne vois pas où est le problème. Ce que vous décrivez ne se produira que si vous essayez d'accéder au «nouvel» état après avoir appelé setState. C'est un problème entièrement différent qui n'a rien à voir avec la question du PO. Accéder à l'ancien état avant d'appeler setState afin de pouvoir construire un nouvel objet d'état ne serait jamais un problème, et c'est précisément ce que React attend.
nextgentech
@nextgentech "Accéder à l'ancien état avant d'appeler setState afin de pouvoir construire un nouvel objet d'état ne serait jamais un problème" - vous vous trompez. Nous parlons de l' état d' écrasement basé sur this.state, qui peut être périmé (ou plutôt en attente de mise à jour en file d'attente) lorsque vous y accédez.
Madbreaks
1
@Madbreaks Ok, oui, en relisant la documentation officielle, je vois qu'il est spécifié que vous devez utiliser la forme de fonction de setState pour mettre à jour de manière fiable l'état en fonction de l'état précédent. Cela dit, je n'ai jamais rencontré de problème à ce jour si je n'essaye pas d'appeler setState plusieurs fois dans la même fonction. Merci pour les réponses qui m'ont permis de relire les spécifications.
nextgentech
10

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

Au lieu de:

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

Faites ceci à la place:

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

Ou, grâce à @ icc97 dans les commentaires, encore plus succinctement mais sans doute moins lisible:

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

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

Ryan Shillington
la source
Upvote, vérifiez. Vous vous demandez simplement pourquoi ne pas le faire comme ça? this.setState({ selected: Object.assign(this.state.selected, { name: "Barfoo" }));
Justus Romijn
1
@JustusRomijn Malheureusement, vous modifieriez directement l'état actuel. Object.assign(a, b)prend les attributs de b, les insère / les écrase apuis les retourne a. Les gens réagissent deviennent impitoyables si vous modifiez directement l'état.
Ryan Shillington
Exactement. Notez simplement que cela ne fonctionne que pour les copies / assignations superficielles.
Madbreaks
1
@JustusRomijn Vous pouvez le faire comme par assign MDN dans une ligne si vous ajoutez {}comme premier argument: this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });. Cela ne modifie pas l'état d'origine.
icc97
@ icc97 Bien! J'ai ajouté cela à ma réponse.
Ryan Shillington
8

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

Exemple de Lodash :

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

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

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

Je l'ai utilisé mergecar extendélimine les autres propriétés autrement.

Exemple d' immutabilité de React :

import update from "react-addons-update";

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

Ma solution pour ce genre de situation est d'utiliser, comme une autre réponse l'a souligné, les assistants Immuabilité .

Étant donné que la définition de l'état en profondeur est une situation courante, j'ai créé le mixin suivant:

var SeStateInDepthMixin = {
   setStateInDepth: function(updatePath) {
       this.setState(React.addons.update(this.state, updatePath););
   }
};

Ce mixin est inclus dans la plupart de mes composants et je ne l'utilise généralement setStateplus directement.

Avec ce mixin, tout ce que vous avez à faire pour obtenir l'effet souhaité est d'appeler la fonction setStateinDepthde la manière suivante:

setStateInDepth({ selected: { name: { $set: 'Barfoo' }}})

Pour plus d'informations:

Dorian
la source
Désolé pour la réponse tardive, mais votre exemple Mixin semble intéressant. Cependant, si j'ai 2 états imbriqués, par exemple this.state.test.one et this.state.test.two. Est-il exact qu'un seul de ces éléments sera mis à jour et l'autre sera supprimé? Lorsque je mets à jour le premier, si le second est dans l'état, il sera supprimé ...
mamruoc
hy @mamruoc, this.state.test.twosera toujours là après la mise this.state.test.oneà jour en utilisant setStateInDepth({test: {one: {$set: 'new-value'}}}). J'utilise ce code dans tous mes composants afin de contourner la setStatefonction limitée de React .
Dorian
1
J'ai trouvé la solution. J'ai initié en this.state.testtant que Array. Changer en objets, l'a résolu.
mamruoc
2

J'utilise des classes es6, et je me suis retrouvé avec plusieurs objets complexes sur mon état supérieur et j'essayais de rendre mon composant principal plus modulaire, j'ai donc créé un wrapper de classe simple pour garder l'état sur le composant supérieur mais permettre plus de logique locale .

La classe wrapper prend une fonction comme constructeur qui définit une propriété sur l'état du composant principal.

export default class StateWrapper {

    constructor(setState, initialProps = []) {
        this.setState = props => {
            this.state = {...this.state, ...props}
            setState(this.state)
        }
        this.props = initialProps
    }

    render() {
        return(<div>render() not defined</div>)
    }

    component = props => {
        this.props = {...this.props, ...props}
        return this.render()
    }
}

Ensuite, pour chaque propriété complexe de l'état supérieur, je crée une classe StateWrapped. Vous pouvez définir les accessoires par défaut dans le constructeur ici et ils seront définis lorsque la classe est initialisée, vous pouvez vous référer à l'état local pour les valeurs et définir l'état local, faire référence aux fonctions locales et le faire passer dans la chaîne:

class WrappedFoo extends StateWrapper {

    constructor(...props) { 
        super(...props)
        this.state = {foo: "bar"}
    }

    render = () => <div onClick={this.props.onClick||this.onClick}>{this.state.foo}</div>

    onClick = () => this.setState({foo: "baz"})


}

Donc, mon composant de niveau supérieur a juste besoin du constructeur pour définir chaque classe sur sa propriété d'état de niveau supérieur, un rendu simple et toutes les fonctions qui communiquent entre les composants.

class TopComponent extends React.Component {

    constructor(...props) {
        super(...props)

        this.foo = new WrappedFoo(
            props => this.setState({
                fooProps: props
            }) 
        )

        this.foo2 = new WrappedFoo(
            props => this.setState({
                foo2Props: props
            }) 
        )

        this.state = {
            fooProps: this.foo.state,
            foo2Props: this.foo.state,
        }

    }

    render() {
        return(
            <div>
                <this.foo.component onClick={this.onClickFoo} />
                <this.foo2.component />
            </div>
        )
    }

    onClickFoo = () => this.foo2.setState({foo: "foo changed foo2!"})
}

Semble fonctionner assez bien pour mes besoins, gardez à l'esprit que vous ne pouvez pas changer l'état des propriétés que vous attribuez aux composants enveloppés au niveau du composant supérieur car chaque composant enveloppé suit son propre état mais met à jour l'état du composant supérieur à chaque fois qu'il change.


la source
2

À partir de maintenant,

Si l'état suivant dépend de l'état précédent, nous vous recommandons d'utiliser le formulaire de fonction de mise à jour à la place:

selon la documentation https://reactjs.org/docs/react-component.html#setstate , en utilisant:

this.setState((prevState) => {
    return {quantity: prevState.quantity + 1};
});
egidiocs
la source
Merci pour la citation / lien, qui manquait dans les autres réponses.
Madbreaks
1

Solution

Edit: Cette solution utilisait la syntaxe de diffusion . Le but était de créer un objet sans aucune référence à prevState, donc cela prevStatene serait pas modifié. Mais dans mon utilisation, prevStatesemblait être modifié parfois. Donc, pour un clonage parfait sans effets secondaires, nous convertissons maintenant prevStateen JSON, puis en arrière. (L'inspiration pour utiliser JSON est venue de MDN .)

Rappelles toi:

Pas

  1. Faites une copie de la propriété de niveau racinestate que vous souhaitez modifier
  2. Muter ce nouvel objet
  3. Créer un objet de mise à jour
  4. Renvoyer la mise à jour

Les étapes 3 et 4 peuvent être combinées sur une seule ligne.

Exemple

this.setState(prevState => {
    var newSelected = JSON.parse(JSON.stringify(prevState.selected)) //1
    newSelected.name = 'Barfoo'; //2
    var update = { selected: newSelected }; //3
    return update; //4
});

Exemple simplifié:

this.setState(prevState => {
    var selected = JSON.parse(JSON.stringify(prevState.selected)) //1
    selected.name = 'Barfoo'; //2
    return { selected }; //3, 4
});

Cela suit parfaitement les directives React. Basé sur la réponse d' eicksl à une question similaire.

Tim
la source
1

Solution ES6

Nous définissons l'état au départ

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

Nous modifions une propriété à un certain niveau de l'objet d'état:

const { selected: _selected } = this.state
const  selected = { ..._selected, name: 'Barfoo' }
this.setState({selected})
//this.state: { selected: { id: 1, name: 'Barfoo' } }
romain
la source
0

L'état React n'effectue pas la fusion récursive setStatealors qu'il s'attend à ce qu'il n'y ait pas de mises à jour des membres d'état sur place en même temps. Vous devez soit copier vous-même les objets / tableaux inclus (avec array.slice ou Object.assign) ou utiliser la bibliothèque dédiée.

Comme celui-ci. NestedLink prend directement en charge la gestion de l'état React composé.

this.linkAt( 'selected' ).at( 'name' ).set( 'Barfoo' );

De plus, le lien vers le selectedou selected.namepeut être transmis partout comme un accessoire unique et modifié avec set.

gaperton
la source
-2

avez-vous défini l'état initial?

J'utiliserai une partie de mon propre code par exemple:

    getInitialState: function () {
        return {
            dragPosition: {
                top  : 0,
                left : 0
            },
            editValue : "",
            dragging  : false,
            editing   : false
        };
    }

Dans une application sur laquelle je travaille, c'est ainsi que j'ai défini et utilisé l'état. Je crois que setStatevous pouvez alors simplement modifier les états que vous voulez individuellement, je l'ai appelé ainsi:

    onChange: function (event) {
        event.preventDefault();
        event.stopPropagation();
        this.setState({editValue: event.target.value});
    },

Gardez à l'esprit que vous devez définir l'état dans la React.createClassfonction que vous avez appeléegetInitialState

Asherrard
la source
1
quel mixin utilisez-vous dans votre composant? Cela ne fonctionne pas pour moi
Sachin
-3

J'utilise le tmp var pour changer.

changeTheme(v) {
    let tmp = this.state.tableData
    tmp.theme = v
    this.setState({
        tableData : tmp
    })
}
hkongm
la source
2
Ici, tmp.theme = v est un état de mutation. Ce n'est donc pas la méthode recommandée.
almoraleslopez
1
let original = {a:1}; let tmp = original; tmp.a = 5; // original is now === Object {a: 5} Ne faites pas cela, même si cela ne vous a pas encore posé de problèmes.
Chris