web-dev-qa-db-fra.com

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 ceci:

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

Mais l'état de mise à jour comme ça,

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

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

249
Alex Yong

Afin de 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 factice, d'effectuer des opérations dessus, puis de remplacer l'état du composant par l'objet mis à jour.

Désormais, l’opérateur spread ne crée qu’une copie imbriquée au niveau de l’objet. Si votre état est hautement imbriqué comme:

_this.state = {
   someProperty: {
      someOtherProperty: {
          anotherProperty: {
             flag: true
          }
          ..
      }
      ...
   }
   ...
}
_

Vous pouvez définir l'état en utilisant l'opérateur de propagation à 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 devient vraiment moche à mesure que l'état devient de plus en plus imbriqué et par conséquent, je vous recommande d'utiliser le package immutability-helper pour mettre à jour l'état.

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

310
Shubham Khatri

Pour l'écrire sur une ligne

this.setState({ someProperty: { ...this.state.someProperty, flag: false} });
105
Yoseph

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 que React n'est pas conçu pour fonctionner avec des états imbriqués et que toutes les solutions proposées ici ressemblent à des piratages. Ils n'utilisent pas le cadre mais se battent avec. Ils suggèrent d'écrire un code pas aussi clair dans le but douteux de regrouper certaines propriétés. Ils sont donc très intéressants en tant que réponse au défi, mais pratiquement inutiles.

Permet d'imaginer l'état suivant:

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

Que se passera-t-il si vous ne changez qu'une valeur de child1? React ne restituera pas la vue car il utilise une comparaison superficielle et trouvera que la propriété parent n'a pas changé. BTW muter directement l'objet d'état est considéré comme une mauvaise pratique en général.

Vous devez donc recréer tout l'objet parent. Mais dans ce cas, nous rencontrerons un autre problème. React pensera que tous les enfants ont changé leurs valeurs et les restituera tous. Bien sûr, ce 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 une solution simple à partir de la version courte.

64

Avertissement

L'état imbriqué dans React est une conception incorrecte

Lire cette excellente réponse .

Raisonnement derrière cette réponse:

Le paramètre setState de React est simplement une commodité intégrée, mais vous vous rendez vite compte qu'il a ses limites. L'utilisation de propriétés personnalisées et l'utilisation intelligente de forceUpdate vous en donnent beaucoup plus. par exemple:

class MyClass extends React.Component {
    myState = someObject
    inputValue = 42
...

MobX, par exemple, modifie complètement l'état des fossés et utilise des propriétés observables personnalisées.
tilisez des observables à la place de l'état dans les composants React.


la réponse à votre misère - voir exemple ici

Il existe un autre moyen plus court pour mettre à jour une propriété imbriqué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))

C'est effectivement pareil que

this.state.nested.flag = false
this.forceUpdate()

Pour la différence subtile dans ce contexte entre forceUpdate et setState, voir l'exemple lié.

Bien sûr, cela abuse de certains principes de base, car la state devrait être en lecture seule, mais puisque vous abandonnez immédiatement l'ancien État et le remplacez par un nouvel état, tout va bien.

Attention

Même si le composant contenant l’état sera mis à jour et restitué correctement ( sauf ceci) ) , le props ne parviendra pas à se propager aux enfants (voir le commentaire de Spymaster ci-dessous) . Utilisez cette technique uniquement si vous savez ce que vous faites.

Par exemple, vous pouvez transmettre un accessoire plat modifié qui est mis à jour et transmis 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 rendu chaque fois que le composant parent est mis à jour, comme après l'appel de this.setState ou this.forceUpdate dans le parent.

Effets de la mutation de l'état

L'utilisation de états imbriqués et la modification directe de l'état sont dangereux car différents objets peuvent contenir (intentionnellement ou non) des références différentes (plus anciennes) à l'élément state et ne sait pas nécessairement quand mettre à jour (par exemple, lorsqu’on utilise PureComponent ou si shouldComponentUpdate est implémenté pour renvoyer false) OU sont destinés à afficher d'anciennes données, comme dans l'exemple ci-dessous.

Imaginez un scénario qui est supposé restituer des données historiques. La mutation des données sous la main entraînera un comportement inattendu car cela modifiera également les éléments précédents.

state-flowstate-flow-nested

Quoi qu'il en soit, ici, vous pouvez voir que Nested PureChildClass n'est pas rendu à nouveau car les accessoires ne se propagent pas.

47
Qwerty

Si vous utilisez ES2015, vous avez accès à Object.assign. 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 renvoyé pour mettre à jour l'état.

Edit: Ajout d’un objet vide en tant que cible à la fonction assign pour s’assurer que l’état n’est pas muté directement comme le fait remarquer carkod.

21
Alyssa Roose

Il existe de nombreuses bibliothèques pour vous aider. Par exemple, en utilisant immutability-helper :

import update from 'immutability-helper';

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

Utilisation de lodash/fp set:

import {set} from 'lodash/fp';

const newState = set(["someProperty", "flag"], false, this.state);

Utilisation de lodash/fp fusion:

import {merge} from 'lodash/fp';

const newState = merge(this.state, {
  someProperty: {flag: false},
});
13
tokland

Avec ES6:

this.setState({...this.state, property: {nestedProperty: "new value"}})

Ce qui équivaut à:

const newState = Object.assign({}, this.state);
newState.property.nestedProperty = "new value";
this.setState(newState);
12
enzolito

Nous utilisons Immer https://github.com/mweststrate/immer pour traiter ce type de problèmes.

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

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

Avec ça

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 passe derrière la scène avec des objets proxy.

6
Joakim Jäderberg

Voici une variante de la première réponse donnée dans ce fil de discussion qui ne nécessite aucun paquet supplémentaire, bibliothèque ou fonction spéciale.

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. Je l’ai fait en créant une variable newState et en y diffusant d’abord le contenu de l’état actuel à l’aide de l’ES2015 propagation opérateur . Ensuite, j'ai remplacé la valeur de this.state.flag par la nouvelle valeur (depuis que j'ai défini flag: value après , j'ai étendu l'état actuel à l'objet, le champ flag dans l'état actuel est remplacé). Ensuite, je règle simplement l'état de someProperty à mon objet newState.

5
Matthew Berkompas

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éaffecte avec les 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 html avec l'écouteur d'événement

<input type="text" onChange={this.handleChange} " name="friendName" />
3
Alberto Piras

Créer une copie de l'état:
let someProperty = JSON.parse(JSON.stringify(this.state.someProperty))

apporter des modifications à cet objet:
someProperty.flag = "false"

maintenant mettre à jour l'état
this.setState({someProperty})

3
Dhakad

Bien que l'imbrication ne soit pas vraiment la façon dont vous devriez traiter un état de composant, parfois quelque chose de facile pour une imbrication à un niveau.

Pour un état comme celui-ci

state = {
 contact: {
  phone: '888-888-8888',
  email: '[email protected]'
 }
 address: {
  street:''
 },
 occupation: {
 }
}

Une méthode réutilisable que ive aurait utilisée ressemblerait à ceci.

handleChange = (obj) => e => {
  let x = this.state[obj];
  x[e.target.name] = e.target.value;
  this.setState({ [obj]: x });
};

puis en passant juste le nom de l'obj pour chaque imbrication que vous voulez adresser ...

<TextField
 name="street"
 onChange={handleChange('address')}
 />
3
user2208124

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

objet d'état

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"
/>

méthode updateState

setState comme la réponse de @ ShubhamKhatri

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 comme la réponse de @ Qwerty

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

Remarque: ces méthodes ci-dessus ne fonctionneront pas pour les tableaux.

2
Venugopal

Je prends très au sérieux les préoccupations déjà exprimées concernant la création d'une copie complète de l'état de votre composant. Cela dit, je suggérerais fortement Immer.

import produce from 'immer';

<Input
  value={this.state.form.username}
  onChange={e => produce(this.state, s => { s.form.username = e.target.value }) } />

Cela devrait fonctionner pour React.PureComponent (c'est-à-dire des comparaisons d'états superficiels par React), car Immer utilise habilement un objet proxy pour copier efficacement un arbre d'états arbitrairement profond. Immer est également plus typé que les bibliothèques telles que Immutability Helper, et est idéal pour les utilisateurs de Javascript et de TypeScript.


Fonction utilitaire TypeScript

function setStateDeep<S>(comp: React.Component<any, S, any>, fn: (s: 
Draft<Readonly<S>>) => any) {
  comp.setState(produce(comp.state, s => { fn(s); }))
}

onChange={e => setStateDeep(this, s => s.form.username = e.target.value)}
2
Stephen Paul

Deux autres options non encore mentionnées:

  1. Si vous avez un état profondément imbriqué, demandez-vous si vous pouvez restructurer les objets enfants pour qu'ils soient assis à la racine. Cela facilite la mise à jour des données.
  2. Il existe de nombreuses bibliothèques pratiques pour gérer l'état immuable répertoriées dans la documentation Redux . Je recommande Immer, car cela vous permet d'écrire du code de manière mutative, mais gère le clonage nécessaire en arrière-plan. Il gèle également l'objet résultant afin que vous ne puissiez pas le muter accidentellement plus tard.
2
Cory House
stateUpdate = () => {
    let obj = this.state;
    if(this.props.v12_data.values.email) {
      obj.obj_v12.Customer.EmailAddress = this.props.v12_data.values.email
    }
    this.setState(obj)
}
1
user3444748

Je sais que c’est une vieille question, mais je voulais tout de même dire comment j’ai réalisé cela. En supposant que l'état dans le constructeur ressemble à ceci:

  constructor(props) {
    super(props);

    this.state = {
      loading: false,
      user: {
        email: ""
      },
      organization: {
        name: ""
      }
    };

    this.handleChange = this.handleChange.bind(this);
  }

Ma fonction handleChange ressemble à ceci:

  handleChange(e) {
    const names = e.target.name.split(".");
    const value = e.target.type === "checkbox" ? e.target.checked : e.target.value;
    this.setState((state) => {
      state[names[0]][names[1]] = value;
      return {[names[0]]: state[names[0]]};
    });
  }

Et assurez-vous de nommer les entrées en conséquence:

<input
   type="text"
   name="user.email"
   onChange={this.handleChange}
   value={this.state.user.firstName}
   placeholder="Email Address"
/>

<input
   type="text"
   name="organization.name"
   onChange={this.handleChange}
   value={this.state.organization.name}
   placeholder="Organization Name"
/>
0
Elnoor

J'ai trouvé que cela fonctionnait pour moi, ayant un formulaire de projet dans mon cas où, par exemple, vous avez un identifiant, un nom et je préfère conserver l'état pour un projet imbriqué.

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

Faites le moi savoir!

0
Michael Stokes

Je fais des mises à jour imbriquées avec un réduire recherche:

Exemple:

Les variables imbriquées à l'état:

state = {
    coords: {
        x: 0,
        y: 0,
        z: 0
    }
}

La fonction:

handleChange = nestedAttr => event => {
  const { target: { value } } = event;
  const attrs = nestedAttr.split('.');

  let stateVar = this.state[attrs[0]];
  if(attrs.length>1)
    attrs.reduce((a,b,index,arr)=>{
      if(index==arr.length-1)
        a[b] = value;
      else if(a[b]!=null)
        return a[b]
      else
        return a;
    },stateVar);
  else
    stateVar = value;

  this.setState({[attrs[0]]: stateVar})
}

Utilisation:

<input
value={this.state.coords.x}
onChange={this.handleTextChange('coords.x')}
/>
0
Emisael Carrera

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
Eladian