javascript - props - Cómo actualizar las propiedades de estado anidado en React
paginas hechas con react js (20)
Estoy tratando de organizar mi estado usando propiedades anidadas como esta:
this.state = {
someProperty: {
flag:true
}
}
Pero actualizar estado como este,
this.setState({ someProperty.flag: false });
no funciona ¿Cómo se puede hacer esto correctamente?
Renuncia
El estado anidado en React es un diseño incorrecto
Lee this .
Razonamiento detrás de esta respuesta:
SetState de React es solo una conveniencia incorporada, pero pronto te das cuenta de que tiene sus límites. El uso de propiedades personalizadas y el uso inteligente de forceUpdate le brinda mucho más. p.ej:
class MyClass extends React.Component { myState = someObject inputValue = 42 ...
MobX, por ejemplo, abandona el estado por completo y utiliza propiedades observables personalizadas.
Use Observables en lugar de estado en React components.
la respuesta a tu miseria - mira el ejemplo aquí
Hay otra forma más corta de actualizar cualquier propiedad anidada.
this.setState(state => {
state.nested.flag = false
state.another.deep.prop = true
return state
})
En una linea
this.setState(state => (state.nested.flag = false, state))
Esto es efectivamente igual que
this.state.nested.flag = false
this.forceUpdate()
Para la sutil diferencia en este contexto entre
forceUpdate
y
setState
vea el ejemplo vinculado.
Por supuesto, esto está abusando de algunos principios básicos, ya que el
state
debe ser de solo lectura, pero dado que descarta inmediatamente el estado anterior y lo reemplaza por un estado nuevo, está completamente bien.
Advertencia
A pesar de que el componente que contiene el estado se actualizará y volverá a mostrar correctamente ( excepto este problema ) , los accesorios no se propagarán a los niños (consulte el comentario de Spymaster a continuación) . Solo use esta técnica si sabe lo que está haciendo.
Por ejemplo, puede pasar un accesorio plano modificado que se actualiza y pasa fácilmente.
render(
//some complex render with your nested state
<ChildComponent complexNestedProp={this.state.nested} pleaseRerender={Math.random()}/>
)
Ahora, aunque la referencia para complexNestedProp no cambió ( shouldComponentUpdate )
this.props.complexNestedProp === nextProps.complexNestedProp
el componente se
volverá a
generar cada vez que se actualice el componente principal, que es el caso después de llamar
this.setState
o
this.forceUpdate
en el elemento primario.
Efectos de mutar el estado
Usar
estado anidado
y mutar el estado directamente es peligroso porque diferentes objetos pueden contener (intencionalmente o no) referencias diferentes (más antiguas) al
estado
y no necesariamente saber cuándo actualizar (por ejemplo, cuando se usa
PureComponent
o si
shouldComponentUpdate
se implementa para devolver
false
)
O
están destinados a mostrar datos antiguos como en el siguiente ejemplo.
Imagine una línea de tiempo que se supone que representa datos históricos, mutar los datos debajo de la mano dará como resultado un comportamiento inesperado, ya que también cambiará los elementos anteriores.
De todos modos, aquí puede ver que
Nested PureChildClass
no se volvió a procesar debido a que los accesorios no se propagan.
Algo como esto podría ser suficiente,
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)
Aquí hay una variación en la primera respuesta dada en este hilo que no requiere ningún paquete adicional, bibliotecas o funciones especiales.
state = {
someProperty: {
flag: ''string''
}
}
handleChange = (value) => {
const newState = {...this.state.someProperty, flag: value}
this.setState({ someProperty: newState })
}
Para establecer el estado de un campo anidado específico, ha configurado todo el objeto.
Hice esto creando una variable,
newState
y extendiendo el contenido del estado actual
primero
usando el
operador de propagación
ES2015.
Luego, reemplacé el valor de
this.state.flag
con el nuevo valor (ya que configuré
flag: value
después de
extender el estado actual en el objeto, el campo de
flag
en el estado actual se anula).
Luego, simplemente configuro el estado de
someProperty
en mi objeto
newState
.
Aunque la anidación no es realmente cómo debe tratar un estado de componente, a veces es algo fácil para la anidación de un solo nivel.
Por un estado como este
state = {
contact: {
phone: ''888-888-8888'',
email: ''[email protected]''
}
address: {
street:''''
},
occupation: {
}
}
Un método reutilizable que he usado se vería así.
handleChange = (obj) => e => {
let x = this.state[obj];
x[e.target.name] = e.target.value;
this.setState({ [obj]: x });
};
luego simplemente pasa el nombre del obj para cada anidamiento que desea abordar ...
<TextField
name="street"
onChange={handleChange(''address'')}
/>
Con ES6:
this.setState({...this.state, property: {nestedProperty: "new value"}})
Lo que es equivalente a:
const newState = Object.assign({}, this.state);
newState.property.nestedProperty = "new value";
this.setState(newState);
Crea una copia del estado:
let someProperty = JSON.parse(JSON.stringify(this.state.someProperty))
hacer cambios en este objeto:
someProperty.flag = "false"
ahora actualiza el estado
this.setState({someProperty})
Encontré que esto funciona para mí, tener un formulario de proyecto en mi caso donde, por ejemplo, tiene una identificación y un nombre y prefiero mantener el estado de un proyecto anidado.
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>
)
¡Házmelo saber!
Hago actualizaciones anidadas con una búsqueda reducida :
Ejemplo:
Las variables anidadas en estado:
state = { coords: { x: 0, y: 0, z: 0 } }
La función:
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}) }
Utilizar:
<input value={this.state.coords.x} onChange={this.handleTextChange(''coords.x'')} />
Hay muchas bibliotecas para ayudar con esto. Por ejemplo, usando immutability-helper :
import update from ''immutability-helper'';
const newState = update(this.state, {
someProperty: {flag: {$set: false}},
};
this.setState(newState);
Usando lodash/fp set:
import {set} from ''lodash/fp'';
const newState = set(["someProperty", "flag"], false, this.state);
Usando lodash/fp merge:
import {merge} from ''lodash/fp'';
const newState = merge(this.state, {
someProperty: {flag: false},
});
Me tomo muy en serio las preocupaciones this torno a la creación de una copia completa del estado de su componente. Dicho esto, sugeriría encarecidamente a https://github.com/mweststrate/immer .
import produce from ''immer'';
<Input
value={this.state.form.username}
onChange={e => produce(this.state, s => { s.form.username = e.target.value }) } />
Esto debería funcionar para
React.PureComponent
(es decir, las comparaciones de estado superficial por React) ya que
Immer
usa inteligentemente un objeto proxy para copiar eficientemente un árbol de estado arbitrariamente profundo.
Immer también es más seguro para los tipos de letra en comparación con bibliotecas como Immutability Helper, y es ideal para usuarios de Javascript y Typecript por igual.
Función de utilidad mecanografiada
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)}
Otras dos opciones aún no mencionadas:
- Si tiene un estado profundamente anidado, considere si puede reestructurar los objetos secundarios para sentarse en la raíz. Esto hace que los datos sean más fáciles de actualizar.
- Hay muchas bibliotecas útiles disponibles para manejar el estado inmutable que figuran en los documentos de Redux . Recomiendo Immer ya que le permite escribir código de manera mutante pero maneja la clonación necesaria detrás de escena. También congela el objeto resultante para que no pueda mutarlo accidentalmente más tarde.
Para escribir en una línea
this.setState({ someProperty: { ...this.state.someProperty, flag: false} });
Para establecer
setState
para un objeto anidado, puede seguir el siguiente enfoque, ya que creo que setState no maneja actualizaciones anidadas.
var someProperty = {...this.state.someProperty}
someProperty.flag = true;
this.setState({someProperty})
La idea es crear un objeto ficticio realizar operaciones en él y luego reemplazar el estado del componente con el objeto actualizado
Ahora, el operador de propagación crea solo una copia anidada de nivel del objeto. Si su estado está altamente anidado como:
this.state = {
someProperty: {
someOtherProperty: {
anotherProperty: {
flag: true
}
..
}
...
}
...
}
Puede configurar State utilizando el operador de propagación en cada nivel como
this.setState(prevState => ({
...prevState,
someProperty: {
...prevState.someProperty,
someOtherProperty: {
...prevState.someProperty.someOtherProperty,
anotherProperty: {
...prevState.someProperty.someOtherProperty.anotherProperty,
flag: false
}
}
}
}))
Sin embargo, la sintaxis anterior se vuelve fea a medida que el estado se anida cada vez más y, por lo tanto, le recomiendo que use el paquete
immutability-helper
para actualizar el estado.
Consulte esta respuesta sobre cómo actualizar el estado con el
immutability helper
.
Para hacer las cosas genéricas, trabajé en las respuestas de @ ShubhamKhatri y @ Qwerty.
objeto de estado
this.state = {
name: '''',
grandParent: {
parent1: {
child: ''''
},
parent2: {
child: ''''
}
}
};
controles de entrada
<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étodo updateState
setState como la respuesta 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 como la respuesta 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);
}
Nota: estos métodos anteriores no funcionarán para matrices
Sé que es una vieja pregunta, pero aún quería compartir cómo lo logré. Asumiendo que el estado en el constructor se ve así:
constructor(props) {
super(props);
this.state = {
loading: false,
user: {
email: ""
},
organization: {
name: ""
}
};
this.handleChange = this.handleChange.bind(this);
}
Mi función
handleChange
es así:
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]]};
});
}
Y asegúrese de nombrar las entradas en consecuencia:
<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"
/>
Si está utilizando ES2015, tiene acceso a Object.assign. Puede usarlo de la siguiente manera para actualizar un objeto anidado.
this.setState({
someProperty: Object.assign({}, this.state.someProperty, {flag: false})
});
Combina las propiedades actualizadas con las existentes y utiliza el objeto devuelto para actualizar el estado.
Editar: se agregó un objeto vacío como objetivo a la función de asignación para asegurarse de que el estado no esté mutado directamente como señaló Carkod.
Usé esta solución.
Si tiene un estado anidado como este:
this.state = {
formInputs:{
friendName:{
value:'''',
isValid:false,
errorMsg:''''
},
friendEmail:{
value:'''',
isValid:false,
errorMsg:''''
}
}
puede declarar la función handleChange que copia el estado actual y la reasigna con valores cambiados
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);
}
aquí el html con el oyente de eventos
<input type="text" onChange={this.handleChange} " name="friendName" />
Usamos Immer https://github.com/mweststrate/immer para manejar este tipo de problemas.
Acabo de reemplazar este código en uno de nuestros componentes
this.setState(prevState => ({
...prevState,
preferences: {
...prevState.preferences,
[key]: newValue
}
}));
Con este
import produce from ''immer'';
this.setState(produce(draft => {
draft.preferences[key] = newValue;
}));
Con immer manejas tu estado como un "objeto normal". La magia ocurre detrás de escena con objetos proxy.
A veces las respuestas directas no son las mejores :)
Version corta:
este codigo
this.state = {
someProperty: {
flag: true
}
}
debería simplificarse como algo así
this.state = {
somePropertyFlag: true
}
Versión larga:
Actualmente no debería querer trabajar con estado anidado en React . Porque React no está orientado a trabajar con estados anidados y todas las soluciones propuestas aquí parecen piratas informáticos. No usan el marco pero luchan con él. Sugieren escribir código no tan claro con el propósito dudoso de agrupar algunas propiedades. Por lo tanto, son muy interesantes como respuesta al desafío, pero prácticamente inútiles.
Imaginemos el siguiente estado:
{
parent: {
child1: ''value 1'',
child2: ''value 2'',
...
child100: ''value 100''
}
}
¿Qué sucederá si cambia solo un valor de
child1
?
React no volverá a representar la vista porque usa una comparación superficial y descubrirá que
parent
propiedad
parent
no cambió.
Por cierto, mutar el objeto de estado directamente se considera una mala práctica en general.
Por lo tanto, debe volver a crear todo el objeto
parent
.
Pero en este caso nos encontraremos con otro problema.
Reaccionar pensará que todos los niños han cambiado sus valores y los volverá a representar a todos.
Por supuesto, no es bueno para el rendimiento.
Todavía es posible resolver ese problema escribiendo una lógica complicada en
shouldComponentUpdate()
pero preferiría detenerme aquí y usar una solución simple de la versión corta.
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)
}