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?
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
.
Pour l'écrire sur une ligne
this.setState({ someProperty: { ...this.state.someProperty, flag: false} });
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.
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.
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
})
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.
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.
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.
Quoi qu'il en soit, ici, vous pouvez voir que Nested PureChildClass
n'est pas rendu à nouveau car les accessoires ne se propagent pas.
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.
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},
});
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);
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.
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
.
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" />
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'étatthis.setState({someProperty})
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')}
/>
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.
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)}
Deux autres options non encore mentionnées:
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)
}
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"
/>
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!
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')}
/>
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)