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?
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
.
De l'écrire en une seule ligne
this.setState({ someProperty: { ...this.state.someProperty, flag: false} });
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é.
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.
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 .
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},
});
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
.
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);
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" />
deux autres options non encore mentionnées:
- 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.
- 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.
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
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.
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!
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)