ce.setState ne fusionne pas les États comme je m'y attendais

J'ai l'état suivant:

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

Ensuite, je mets à jour l'état:

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

Puisque setState est supposé fusionner, Je m'attendrais à ce qu'il soit:

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

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

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

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

141
demandé sur Dmitry Shvedov 2013-09-21 18:50:46

12 réponses

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

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

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

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

Une Autre solution serait d'écrire setStateRecursively(), qui n'est récursive de fusion sur un nouvel état, puis appelle replaceState() avec:

setStateRecursively: function(stateUpdate, callback) {
  var newState = mergeStateRecursively(this.state, stateUpdate);
  this.replaceState(newState, callback);
}
132
répondu andreypopp 2013-09-21 15:29:44

Des aides à L'immuabilité ont récemment été ajoutées pour réagir.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 à L'immuabilité: http://facebook.github.io/react/docs/update.html

94
répondu user3874611 2014-09-07 18:21:25

Comme beaucoup de réponses utilisent l'état actuel comme base pour fusionner de nouvelles données, je voulais souligner que cela peut casser. Les modifications d'état sont mises en file d'attente et ne modifient pas immédiatement l'objet d'état d'un composant. Référencer les données d'état avant que la file d'attente ait été traitée 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. De la docs:

SetState () ne mute pas immédiatement cela.état mais crée un État en attente transition. L'accès à ce.l'état 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 ultérieurs à 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, vous souhaitez donc effectuer une fusion. Avant d'appeler setState, vous obtenez l'objet état actuel. Cet objet ne reflète pas la file d'attente modifications apportées au premier appel à setState, ci-dessus, car c'est toujours l'état d'origine, qui devrait maintenant être considéré comme "périmé".
  3. effectuer une 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 setState initial ne sont pas reflétées. Votre appel setState met en file d'attente ce deuxième changement.
  4. React processus 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 du second setState a maintenant remplacé le tout d'abord, et puisque 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 bloqué 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 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 alternativement accepte une fonction en tant qu'argument au lieu d'un objet; la fonction est appelée après toutes les mises à jour précédentes de state, et passe l'état en tant qu'argument-donc cela peut être utilisé pour faire des changements atomiques garantis pour respecter les changements précédents.

31
répondu bgannonpl 2016-02-02 17:03:48

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

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

8
répondu Ryan Shillington 2018-04-16 17:21:12

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

Lodash exemple:

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

J'ai utilisé merge parce que extend rejette les autres propriétés autrement.

Réagir Immutabilité exemple:

import update from "react-addons-update";

this.setState((previousState) => update(previousState, {
    selected:
    { 
        name: {$set: "Barfood"}
    }
});
7
répondu Martin Dawson 2018-01-31 18:11:39

Ma solution pour ce genre de situation est d'utiliser, comme une autre réponse l'a souligné, Les aides à L'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 n'utilise généralement plus setState directement.

, Avec ce mixin, tout ce que vous devez faire pour obtenir l'effet désiré est d'appeler la fonction setStateinDepth dans la suite façon:

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

Pour plus d'informations:

2
répondu Dorian 2016-02-02 17:04:14

Dès maintenant,

Si le prochain état dépend de l'état précédent, nous vous recommandons d'utiliser la forme 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};
});
2
répondu egidiocs 2017-11-26 01:10:50

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, donc j'ai 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 sur 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, se référer 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 affectez aux composants encapsulés au composant de niveau supérieur car chaque composant encapsulé suit son propre état mais met à jour l'état du composant supérieur chaque fois qu'il change.

1
répondu 2016-05-09 01:30:14

Solution

Edit: cette solution utilisait la syntaxe de propagation . Le but était de faire un objet sans aucune référence à prevState, de sorte que prevState ne serait pas modifié. Mais dans mon utilisation, prevState semblait être modifié parfois. Donc, pour un clonage parfait sans effets secondaires, nous convertissons maintenant prevState en JSON, puis de nouveau. (L'Inspiration pour utiliser JSON est venue de MDN .)

Rappelez-vous:

Étapes

  1. faites une copie de la propriété racine de state que vous souhaitez modifier
  2. Muter ce nouvel objet
  3. créer un objet de mise à jour
  4. renvoie la mise à jour

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

Ceci suit les directives React bien. Basé sur réponse de eicksl à une question similaire.

1
répondu Tim 2018-08-01 15:38:50

L'état React n'effectue pas la fusion récursive dans setState alors qu'il s'attend à ce qu'il n'y ait pas de mises à jour de membre d'état sur place en même temps. Vous devez soit Copier vous-même des objets/tableaux fermés (avec array.tranche ou d'un Objet.assign) ou utilisez la bibliothèque dédiée.

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

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

En outre, le lien vers le selected ou selected.name peut être passé partout comme un seul accessoire et modifié là-bas avec set.

0
répondu gaperton 2017-04-19 19:58:06

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

Je vais utiliser un peu 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, voici comment j'ai défini et utilisé l'état. Je crois que sur setState Vous pouvez ensuite modifier les états que vous voulez individuellement, Je l'ai appelé comme suit:

    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 fonction React.createClass que vous avez appelée getInitialState

-2
répondu asherrard 2013-11-22 22:29:32

J'utilise la var tmp pour changer.

changeTheme(v) {
    let tmp = this.state.tableData
    tmp.theme = v
    this.setState({
        tableData : tmp
    })
}
-2
répondu hkongm 2015-07-06 09:34:57