Comment mettre à jour les propriétés d'état imbriquées dans React

j'essaie d'organiser mon état en utilisant une propriété imbriquée comme celle-ci:

this.state = {
   someProperty: {
      flag:true
   }
}

mais l'état de mise à jour comme ceci,

this.setState({ someProperty.flag: false });

ne marche pas. Comment cela peut-il être fait correctement?

96
demandé sur Dan Dascalescu 2017-03-27 10:51:38

14 réponses

pour setState pour un objet imbriqué, vous pouvez suivre l'approche ci-dessous car je pense que setState ne gère pas les mises à jour imbriquées.

var someProperty = {...this.state.someProperty}
someProperty.flag = true;
this.setState({someProperty})

l'idée est de créer un objet fictif effectuer des opérations sur celui-ci, puis de remplacer l'état du composant par l'objet mis à jour

maintenant, l'opérateur spread ne crée qu'un niveau de copie imbriquée de l'objet. Si votre état est fortement imbriqué comme:

this.state = {
   someProperty: {
      someOtherProperty: {
          anotherProperty: {
             flag: true
          }
          ..
      }
      ...
   }
   ...
}

You pourrait setState en utilisant l'opérateur spread à chaque niveau comme

this.setState(prevState => ({
    ...prevState,
    someProperty: {
        ...prevState.someProperty,
        someOtherProperty: {
            ...prevState.someProperty.someOtherProperty, 
            anotherProperty: {
               ...prevState.someProperty.someOtherProperty.anotherProperty,
               flag: false
            }
        }
    }
}))

cependant la syntaxe ci-dessus deviennent chaque laid que l'état devient de plus en plus imbriqué et donc je vous recommande d'utiliser immutability-helper paquet pour mettre à jour l'état.

voir cette réponse sur la façon de mettre à jour l'état avec immutability helper .

142
répondu Shubham Khatri 2018-04-10 18:55:00

De l'écrire en une seule ligne

this.setState({ someProperty: { ...this.state.someProperty, flag: false} });
50
répondu Yoseph 2017-08-09 22:07:17

si vous utilisez ES2015 vous avez accès à l'objet.assigner. Vous pouvez l'utiliser comme suit pour mettre à jour un objet imbriqué.

this.setState({
  someProperty: Object.assign({}, this.state.someProperty, {flag: false})
});

vous fusionnez les propriétés mises à jour avec les propriétés existantes et utilisez l'objet retourné pour mettre à jour l'état.

Edit: ajout d'un objet vide comme cible à la fonction assign pour s'assurer que l'état n'est pas muté directement comme carkod l'a souligné.

17
répondu Alyssa Roose 2018-02-07 06:58:45

parfois, les réponses directes ne sont pas les meilleures :)

version Courte:

ce code

this.state = {
    someProperty: {
        flag: true
    }
}

devrait être simplifié comme quelque chose comme

this.state = {
    somePropertyFlag: true
}

version longue:

actuellement vous ne devriez pas vouloir travailler avec l'état imbriqué dans React . Parce React n'est pas orienté à travailler avec des États imbriqués et toutes les solutions proposées ici semblent être des hacks. Ils n'utilisent pas le cadre, mais se battent avec. Ils suggèrent d'écrire un code pas si clair dans un but douteux de regrouper certaines propriétés. Ils sont donc très intéressants comme réponse au défi mais pratiquement inutiles.

imaginons l'état suivant:

{
    parent: {
        child1: 'value 1',
        child2: 'value 2',
        ...
        child100: 'value 100'
    }
}

que se passera-t-il si vous changez juste une valeur de child1 ? Va réagir ne pas re-rendre la vue parce qu'il utilise une comparaison superficielle et il constatera que la propriété parent n'a pas changé. La mutation directe de L'objet d'état par BTW est considérée comme une mauvaise pratique en général.

vous devez donc recréer l'objet parent en entier. Mais dans ce cas, nous rencontrerons un autre problème. React pensera que tous les enfants ont changé leurs valeurs et les rejaillira tous. Bien sûr, il n'est pas bon pour la performance.

Il est encore possible de résoudre ce problème en écrivant une logique compliquée dans shouldComponentUpdate() mais je préférerais m'arrêter ici et utiliser la solution simple de la version courte.

13
répondu Konstantin Smolyanin 2018-07-02 12:18:31

le meilleur de tous - voir l'exemple ici

il y a une autre plus courte façon de mettre à jour n'importe quelle propriété emboîtée.

this.setState(state => {
  state.nested.flag = false
  state.another.deep.prop = true
  return state
})

sur une ligne

this.setState(state => (state.nested.flag = false, state))

bien sûr, Ceci est un abus de certains principes de base, comme le state devrait être lu seulement, mais puisque vous êtes immédiatement jeter l'ancien l'état et le remplacer par un nouvel état, c'est tout à fait correct.

Avertissement

même si le composant contenant l'état sera update and rerender properly ( sauf ce gotcha ) , les accessoires fail to propagate to children (voir le commentaire de Spymaster ci-dessous) . Seulement utiliser cette technique si vous savez ce que vous faire.

par exemple, vous pouvez passer un accessoire plat modifié qui est mis à jour et passé facilement.

render(
  //some complex render with your nested state
  <ChildComponent complexNestedProp={this.state.nested} pleaseRerender={Math.random()}/>
)

maintenant même si la référence pour complexNestedProp n'a pas changé ( shouldComponentUpdate )

this.props.complexNestedProp === nextProps.complexNestedProp

le composant sera chaque fois que le composant parent sera mis à jour, ce qui est le cas après avoir appelé this.setState ou this.forceUpdate dans le parent.

Avertissement

L'État imbriqué dans React est une mauvaise conception

Lire cette excellente réponse .

6
répondu Qwerty 2018-08-10 09:02:07

il y a beaucoup de bibliothèques pour aider avec cela. Par exemple, en utilisant immutabilité-helper :

import update from 'immutability-helper';

const newState = update(this.state, {
  someProperty: {flag: {$set: false}},
};
this.setState(newState);

ou en utilisant lodash / fp :

import {merge} from 'lodash/fp';

const newState = merge(this.state, {
  someProperty: {flag: false},
});
6
répondu tokland 2018-08-13 18:42:37

voici une variation sur la première réponse donnée dans ce thread qui ne nécessite pas de paquets supplémentaires, bibliothèques ou fonctions spéciales.

state = {
  someProperty: {
    flag: 'string'
  }
}

handleChange = (value) => {
  const newState = {...this.state.someProperty, flag: value}
  this.setState({ someProperty: newState })
}

pour définir l'état d'un champ imbriqué spécifique, vous avez défini l'objet entier. J'ai fait cela en créant une variable, newState et en étalant le contenu de l'état actuel dans elle première en utilisant L'ES2015 opérateur de propagation . Ensuite, j'ai remplacé le valeur de this.state.flag avec la nouvelle valeur (puisque j'ai placé flag: value après j'ai étendu l'état courant dans l'objet, le champ flag dans l'état courant est dépassé). Ensuite, je mets simplement l'état de someProperty sur mon objet newState .

5
répondu Matthew Berkompas 2017-11-29 17:10:44

vous pouvez aussi aller de cette façon (qui se sent plus lisible pour moi):

const newState = Object.assign({}, this.state);
newState.property.nestedProperty = 'newValue';
this.setState(newState);
4
répondu enzolito 2018-04-07 13:30:54

j'ai utilisé cette solution.

Si vous avez un état imbriqué comme ceci:

   this.state = {
          formInputs:{
            friendName:{
              value:'',
              isValid:false,
              errorMsg:''
            },
            friendEmail:{
              value:'',
              isValid:false,
              errorMsg:''
            }
}

vous pouvez déclarer la fonction handleChange qui copie l'état actuel et le ré-attribue avec des valeurs modifiées

handleChange(el) {
    let inputName = el.target.name;
    let inputValue = el.target.value;

    let statusCopy = Object.assign({}, this.state);
    statusCopy.formInputs[inputName].value = inputValue;

    this.setState(statusCopy);
  }

ici le code html avec l'écouteur d'événement

<input type="text" onChange={this.handleChange} " name="friendName" />
2
répondu Alberto Piras 2018-03-06 09:40:48

deux autres options non encore mentionnées:

  1. si vous avez un état profondément imbriqué, considérez si vous pouvez restructurer les objets enfant pour vous asseoir à la racine. Cela rend les données plus faciles à mettre à jour.
  2. il existe de nombreuses bibliothèques pratiques disponibles pour gérer l'état immuable listées dans le Redux docs . Je recommande Immer car il vous permet d'écrire du code de manière mutative mais traite le clonage nécessaire derrière le scènes. Il gèle également l'objet résultant de sorte que vous ne pouvez pas accidentellement le muter plus tard.
2
répondu Cory House 2018-06-28 02:57:53

pour rendre les choses génériques, j'ai travaillé sur les réponses de @ShubhamKhatri et @Qwerty.

d'état de l'objet

this.state = {
  name: '',
  grandParent: {
    parent1: {
      child: ''
    },
    parent2: {
      child: ''
    }
  }
};

contrôles d'entrée

<input
  value={this.state.name}
  onChange={this.updateState}
  type="text"
  name="name"
/>
<input
  value={this.state.grandParent.parent1.child}
  onChange={this.updateState}
  type="text"
  name="grandParent.parent1.child"
/>
<input
  value={this.state.grandParent.parent2.child}
  onChange={this.updateState}
  type="text"
  name="grandParent.parent2.child"
/>

updateState méthode

setState @ShubhamKhatri la réponse de

updateState(event) {
  const path = event.target.name.split('.');
  const depth = path.length;
  const oldstate = this.state;
  const newstate = { ...oldstate };
  let newStateLevel = newstate;
  let oldStateLevel = oldstate;

  for (let i = 0; i < depth; i += 1) {
    if (i === depth - 1) {
      newStateLevel[path[i]] = event.target.value;
    } else {
      newStateLevel[path[i]] = { ...oldStateLevel[path[i]] };
      oldStateLevel = oldStateLevel[path[i]];
      newStateLevel = newStateLevel[path[i]];
    }
  }
  this.setState(newstate);
}

setState as @Qwarte's answer

updateState(event) {
  const path = event.target.name.split('.');
  const depth = path.length;
  const state = { ...this.state };
  let ref = state;
  for (let i = 0; i < depth; i += 1) {
    if (i === depth - 1) {
      ref[path[i]] = event.target.value;
    } else {
      ref = ref[path[i]];
    }
  }
  this.setState(state);
}

Note: Ces méthodes ci-dessus ne fonctionneront pas pour les tableaux

1
répondu Venugopal 2018-07-02 08:29:00

nous utilisons Immer https://github.com/mweststrate/immer pour traiter ce genre de questions.

vient de remplacer ce code dans l'un de nos composants

this.setState(prevState => ({
   ...prevState,
        preferences: {
            ...prevState.preferences,
            [key]: newValue
        }
}));

avec ce

import produce from 'immer';

this.setState(produce(draft => {
    draft.preferences[key] = newValue;
}));

avec immer vous gérez votre état comme un "objet normal". La magie se produit derrière la scène avec des objets proxy.

1
répondu Joakim Jäderberg 2018-08-24 09:40:36

j'ai trouvé cela pour travailler pour moi, ayant un formulaire de projet dans mon cas où, par exemple, vous avez une id, et un nom et je préfère maintenir l'État pour un projet emboîté.

return (
  <div>
      <h2>Project Details</h2>
      <form>
        <Input label="ID" group type="number" value={this.state.project.id} onChange={(event) => this.setState({ project: {...this.state.project, id: event.target.value}})} />
        <Input label="Name" group type="text" value={this.state.project.name} onChange={(event) => this.setState({ project: {...this.state.project, name: event.target.value}})} />
      </form> 
  </div>
)

prévenez-moi!

0
répondu Michael Stokes 2018-06-13 15:36:11

quelque chose comme ça pourrait suffire,

const isObject = (thing) => {
    if(thing && 
        typeof thing === 'object' &&
        typeof thing !== null
        && !(Array.isArray(thing))
    ){
        return true;
    }
    return false;
}

/*
  Call with an array containing the path to the property you want to access
  And the current component/redux state.

  For example if we want to update `hello` within the following obj
  const obj = {
     somePrimitive:false,
     someNestedObj:{
        hello:1
     }
  }

  we would do :
  //clone the object
  const cloned = clone(['someNestedObj','hello'],obj)
  //Set the new value
  cloned.someNestedObj.hello = 5;

*/
const clone = (arr, state) => {
    let clonedObj = {...state}
    const originalObj = clonedObj;
    arr.forEach(property => {
        if(!(property in clonedObj)){
            throw new Error('State missing property')
        }

        if(isObject(clonedObj[property])){
            clonedObj[property] = {...originalObj[property]};
            clonedObj = clonedObj[property];
        }
    })
    return originalObj;
}

const nestedObj = {
    someProperty:true,
    someNestedObj:{
        someOtherProperty:true
    }
}

const clonedObj = clone(['someProperty'], nestedObj);
console.log(clonedObj === nestedObj) //returns false
console.log(clonedObj.someProperty === nestedObj.someProperty) //returns true
console.log(clonedObj.someNestedObj === nestedObj.someNestedObj) //returns true

console.log()
const clonedObj2 = clone(['someProperty','someNestedObj','someOtherProperty'], nestedObj);
console.log(clonedObj2 === nestedObj) // returns false
console.log(clonedObj2.someNestedObj === nestedObj.someNestedObj) //returns false
//returns true (doesn't attempt to clone because its primitive type)
console.log(clonedObj2.someNestedObj.someOtherProperty === nestedObj.someNestedObj.someOtherProperty) 
0
répondu Eladian 2018-08-29 04:28:33