this.setState no está fusionando estados como esperaría


160

Tengo el siguiente estado:

this.setState({ selected: { id: 1, name: 'Foobar' } });  

Luego actualizo el estado:

this.setState({ selected: { name: 'Barfoo' }});

Como setStatese supone que debe fusionarse, esperaría que fuera:

{ selected: { id: 1, name: 'Barfoo' } }; 

Pero en cambio, se come la identificación y el estado es:

{ selected: { name: 'Barfoo' } }; 

¿Es este comportamiento esperado y cuál es la solución para actualizar solo una propiedad de un objeto de estado anidado?

Respuestas:


143

Creo setState()que no hace una fusión recursiva.

Puede usar el valor del estado actual this.state.selectedpara construir un nuevo estado y luego invocarlo setState():

var newSelected = _.extend({}, this.state.selected);
newSelected.name = 'Barfoo';
this.setState({ selected: newSelected });

He usado la función _.extend()función (de la biblioteca underscore.js) aquí para evitar modificaciones en la selectedparte existente del estado al crear una copia superficial de la misma.

Otra solución sería escribir lo setStateRecursively()que hace una fusión recursiva en un nuevo estado y luego llama replaceState()con él:

setStateRecursively: function(stateUpdate, callback) {
  var newState = mergeStateRecursively(this.state, stateUpdate);
  this.replaceState(newState, callback);
}

77
¿Funciona de manera confiable si lo haces más de una vez o si hay alguna posibilidad de que reaccione, se colocarán en cola las llamadas replaceState y la última ganará?
edoloughlin

111
Para las personas que prefieren usar extend y que también usan babel, puede hacer lo this.setState({ selected: { ...this.state.selected, name: 'barfoo' } })que se traduce athis.setState({ selected: _extends({}, this.state.selected, { name: 'barfoo' }) });
Pickels el

8
@Pickels esto es genial, muy idiomático para React y no requiere underscore/ lodash. Gíralo en su propia respuesta, por favor.
ericsoco

14
this.state no está necesariamente actualizado . Puede obtener resultados inesperados utilizando el método extendido. Pasar una función de actualización a setStatees la solución correcta.
Strom

1
@ EdwardD'Souza Es la biblioteca lodash. Se invoca comúnmente como un guión bajo, de la misma manera que jQuery se invoca comúnmente como un signo de dólar. (Entonces, es _.extend ().)
mjk

96

Los ayudantes de inmutabilidad se agregaron recientemente a React.addons, por lo que con eso, ahora puede hacer algo como:

var newState = React.addons.update(this.state, {
  selected: {
    name: { $set: 'Barfoo' }
  }
});
this.setState(newState);

Documentación de ayuda de inmutabilidad .


77
la actualización está en desuso. facebook.github.io/react/docs/update.html
thedanotto

3
@thedanotto Parece que el React.addons.update()método está en desuso, pero la biblioteca de reemplazo github.com/kolodny/immutability-helper contiene una update()función equivalente .
Bungle

37

Dado que muchas de las respuestas utilizan el estado actual como base para fusionarse en nuevos datos, quería señalar que esto puede romperse. Los cambios de estado se ponen en cola y no modifican inmediatamente el objeto de estado de un componente. Hacer referencia a los datos de estado antes de que se haya procesado la cola, por lo tanto, le dará datos obsoletos que no reflejan los cambios pendientes que realizó en setState. De los documentos:

setState () no muta inmediatamente this.state pero crea una transición de estado pendiente. Acceder a this.state después de llamar a este método puede potencialmente devolver el valor existente.

Esto significa que el uso del estado "actual" como referencia en llamadas posteriores a setState no es confiable. Por ejemplo:

  1. Primera llamada a setState, haciendo cola un cambio al objeto de estado
  2. Segunda llamada a setState. Su estado utiliza objetos anidados, por lo que desea realizar una fusión. Antes de llamar a setState, obtienes el objeto de estado actual. Este objeto no refleja los cambios en cola realizados en la primera llamada a setState, arriba, porque sigue siendo el estado original, que ahora debe considerarse "obsoleto".
  3. Realizar fusión. El resultado es el estado "obsoleto" original más los nuevos datos que acaba de configurar, los cambios de la llamada inicial de setState no se reflejan. Su llamada setState pone en cola este segundo cambio.
  4. Procesos de reacción en cola. Se procesa la primera llamada setState, actualizando el estado. Se procesa la segunda llamada setState, actualizando el estado. El segundo objeto setState ahora ha reemplazado al primero, y dado que los datos que tenía al hacer esa llamada estaban obsoletos, los datos obsoletos modificados de esta segunda llamada han bloqueado los cambios realizados en la primera llamada, que se pierden.
  5. Cuando la cola está vacía, React determina si se procesará, etc. En este punto, representará los cambios realizados en la segunda llamada setState, y será como si la primera llamada setState nunca sucediera.

Si necesita usar el estado actual (por ejemplo, para combinar datos en un objeto anidado), setState acepta alternativamente una función como argumento en lugar de un objeto; la función se llama después de cualquier actualización previa de estado, y pasa el estado como argumento, por lo que esto puede usarse para realizar cambios atómicos garantizados para respetar los cambios anteriores.


55
¿Hay un ejemplo o más documentos sobre cómo hacer esto? afaik, nadie tiene esto en cuenta.
Josef.B

@ Dennis Prueba mi respuesta a continuación.
Martin Dawson

No veo dónde está el problema. Lo que está describiendo solo ocurriría si intenta acceder al estado "nuevo" después de llamar a setState. Ese es un problema completamente diferente que no tiene nada que ver con la pregunta OP. Acceder al estado anterior antes de llamar a setState para que pueda construir un nuevo objeto de estado nunca sería un problema, y ​​de hecho es precisamente lo que React espera.
nextgentech

@nextgentech "Acceder al estado anterior antes de llamar a setState para que pueda construir un nuevo objeto de estado nunca sería un problema" , está equivocado. Estamos hablando de sobrescribir el estado basado en this.state, que puede ser obsoleto (o más bien, pendiente de actualización en cola) cuando accede a él.
Madbreaks

1
@Madbreaks Ok, sí, al volver a leer los documentos oficiales, veo que se especifica que debe usar la forma de la función setState para actualizar de manera confiable el estado en función del estado anterior. Dicho esto, nunca me he encontrado con un problema hasta la fecha si no intento llamar a setState varias veces en la misma función. Gracias por las respuestas que me hicieron volver a leer las especificaciones.
nextgentech

10

No quería instalar otra biblioteca, así que aquí hay otra solución.

En vez de:

this.setState({ selected: { name: 'Barfoo' }});

Haz esto en su lugar:

var newSelected = Object.assign({}, this.state.selected);
newSelected.name = 'Barfoo';
this.setState({ selected: newSelected });

O, gracias a @ icc97 en los comentarios, aún más sucinta pero posiblemente menos legible:

this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });

Además, para ser claros, esta respuesta no viola ninguna de las preocupaciones que @bgannonpl mencionó anteriormente.


Votación a favor, verifique. Me pregunto por qué no hacerlo así. this.setState({ selected: Object.assign(this.state.selected, { name: "Barfoo" }));
Justus Romijn

1
@JustusRomijn Desafortunadamente, estaría modificando el estado actual directamente. Object.assign(a, b)toma atributos b, los inserta / los sobrescribe ay luego los devuelve a. Las personas reaccionan se vuelven optimistas si modificas el estado directamente.
Ryan Shillington

Exactamente. Solo tenga en cuenta que esto solo funciona para la copia / asignación superficial.
Madbreaks

1
@JustusRomijn Usted puede hacer esto de acuerdo con MDN asignar en una línea si se añade {}como primer argumento: this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });. Esto no muta el estado original.
icc97

@ icc97 ¡Bien! Agregué eso a mi respuesta.
Ryan Shillington

8

Preservar el estado anterior basado en la respuesta @bgannonpl:

Ejemplo de Lodash :

this.setState((previousState) => _.merge({}, previousState, { selected: { name: "Barfood"} }));

Para verificar que funcionó correctamente, puede usar la devolución de llamada de la función del segundo parámetro:

this.setState((previousState) => _.merge({}, previousState, { selected: { name: "Barfood"} }), () => alert(this.state.selected));

Solía mergeporque extenddescarta las otras propiedades de lo contrario.

Ejemplo de reacción de inmutabilidad :

import update from "react-addons-update";

this.setState((previousState) => update(previousState, {
    selected:
    { 
        name: {$set: "Barfood"}
    }
});

2

Mi solución para este tipo de situación es usar, como otra respuesta señalada, los ayudantes de Inmutabilidad .

Dado que establecer el estado en profundidad es una situación común, he creado el siguiente mixin:

var SeStateInDepthMixin = {
   setStateInDepth: function(updatePath) {
       this.setState(React.addons.update(this.state, updatePath););
   }
};

Este mixin está incluido en la mayoría de mis componentes y generalmente no uso setState directamente.

Con este mixin, todo lo que necesita hacer para lograr el efecto deseado es llamar a la función setStateinDepthde la siguiente manera:

setStateInDepth({ selected: { name: { $set: 'Barfoo' }}})

Para más información:


Perdón por la respuesta tardía, pero su ejemplo de Mixin parece interesante. Sin embargo, si tengo 2 estados anidados, por ejemplo, this.state.test.one y this.state.test.two. ¿Es correcto que solo uno de estos se actualice y el otro se elimine? Cuando actualizo el primero, si el segundo está en el estado, se eliminará ...
mamruoc

hy @mamruoc, this.state.test.twoseguirá estando allí después de actualizar this.state.test.oneusando setStateInDepth({test: {one: {$set: 'new-value'}}}). Utilizo este código en todos mis componentes para evitar la setStatefunción limitada de React .
Dorian

1
Encontré la solución. Me inicié this.state.testcomo una matriz. Cambiando a Objetos, lo resolvió.
mamruoc

2

Estoy usando clases es6, y terminé con varios objetos complejos en mi estado superior e intentaba hacer que mi componente principal fuera más modular, así que creé un contenedor de clase simple para mantener el estado en el componente superior pero permitir más lógica local .

La clase wrapper toma una función como su constructor que establece una propiedad en el estado del componente principal.

export default class StateWrapper {

    constructor(setState, initialProps = []) {
        this.setState = props => {
            this.state = {...this.state, ...props}
            setState(this.state)
        }
        this.props = initialProps
    }

    render() {
        return(<div>render() not defined</div>)
    }

    component = props => {
        this.props = {...this.props, ...props}
        return this.render()
    }
}

Luego, para cada propiedad compleja en el estado superior, creo una clase StateWrapped. Puede establecer los accesorios predeterminados en el constructor aquí y se establecerán cuando se inicialice la clase, puede consultar el estado local para los valores y establecer el estado local, consultar las funciones locales y hacer que pase por la cadena:

class WrappedFoo extends StateWrapper {

    constructor(...props) { 
        super(...props)
        this.state = {foo: "bar"}
    }

    render = () => <div onClick={this.props.onClick||this.onClick}>{this.state.foo}</div>

    onClick = () => this.setState({foo: "baz"})


}

Entonces, mi componente de nivel superior solo necesita que el constructor establezca cada clase en su propiedad de estado de nivel superior, un render simple y cualquier función que comunique componentes cruzados.

class TopComponent extends React.Component {

    constructor(...props) {
        super(...props)

        this.foo = new WrappedFoo(
            props => this.setState({
                fooProps: props
            }) 
        )

        this.foo2 = new WrappedFoo(
            props => this.setState({
                foo2Props: props
            }) 
        )

        this.state = {
            fooProps: this.foo.state,
            foo2Props: this.foo.state,
        }

    }

    render() {
        return(
            <div>
                <this.foo.component onClick={this.onClickFoo} />
                <this.foo2.component />
            </div>
        )
    }

    onClickFoo = () => this.foo2.setState({foo: "foo changed foo2!"})
}

Parece que funciona bastante bien para mis propósitos, tenga en cuenta que no puede cambiar el estado de las propiedades que asigna a los componentes encapsulados en el componente de nivel superior ya que cada componente encapsulado sigue su propio estado pero actualiza el estado en el componente superior Cada vez que cambia.


2

A partir de ahora,

Si el siguiente estado depende del estado anterior, recomendamos utilizar el formulario de función de actualización, en su lugar:

de acuerdo con la documentación https://reactjs.org/docs/react-component.html#setstate , utilizando:

this.setState((prevState) => {
    return {quantity: prevState.quantity + 1};
});

Gracias por la cita / enlace, que faltaba en otras respuestas.
Madbreaks

1

Solución

Editar: Esta solución solía usar sintaxis de propagación . El objetivo era hacer un objeto sin referencias prevState, para que prevStateno se modificara. Pero en mi uso, prevStateparecía estar modificado a veces. Entonces, para una clonación perfecta sin efectos secundarios, ahora convertimos prevStatea JSON y luego nuevamente. (La inspiración para usar JSON vino de MDN ).

Recuerda:

Pasos

  1. Haga una copia de la propiedad de nivel raíz de la stateque desea cambiar
  2. Mutar este nuevo objeto
  3. Crear un objeto de actualización
  4. Devuelve la actualización

Los pasos 3 y 4 se pueden combinar en una línea.

Ejemplo

this.setState(prevState => {
    var newSelected = JSON.parse(JSON.stringify(prevState.selected)) //1
    newSelected.name = 'Barfoo'; //2
    var update = { selected: newSelected }; //3
    return update; //4
});

Ejemplo simplificado:

this.setState(prevState => {
    var selected = JSON.parse(JSON.stringify(prevState.selected)) //1
    selected.name = 'Barfoo'; //2
    return { selected }; //3, 4
});

Esto sigue las pautas de React muy bien. Basado en la respuesta de eicksl a una pregunta similar.


1

Solución ES6

Establecemos el estado inicialmente

this.setState({ selected: { id: 1, name: 'Foobar' } }); 
//this.state: { selected: { id: 1, name: 'Foobar' } }

Estamos cambiando una propiedad en algún nivel del objeto de estado:

const { selected: _selected } = this.state
const  selected = { ..._selected, name: 'Barfoo' }
this.setState({selected})
//this.state: { selected: { id: 1, name: 'Barfoo' } }

0

El estado de reacción no realiza la fusión recursiva setStatemientras espera que no haya actualizaciones de miembros de estado en el lugar al mismo tiempo. Debe copiar los objetos / matrices adjuntos usted mismo (con array.slice u Object.assign) o usar la biblioteca dedicada.

Como éste. NestedLink admite directamente el manejo del estado React compuesto.

this.linkAt( 'selected' ).at( 'name' ).set( 'Barfoo' );

Además, el enlace al selectedo selected.namese puede pasar a todas partes como un solo accesorio y modificarse allí con set.


-2

¿Has establecido el estado inicial?

Usaré algunos de mi propio código, por ejemplo:

    getInitialState: function () {
        return {
            dragPosition: {
                top  : 0,
                left : 0
            },
            editValue : "",
            dragging  : false,
            editing   : false
        };
    }

En una aplicación en la que estoy trabajando, así es como he estado configurando y usando el estado. Creo setStateque puedes editar los estados que quieras individualmente. Lo he estado llamando así:

    onChange: function (event) {
        event.preventDefault();
        event.stopPropagation();
        this.setState({editValue: event.target.value});
    },

Tenga en cuenta que debe establecer el estado dentro de la React.createClassfunción que llamógetInitialState


1
¿Qué mixin estás usando en tu componente? Esto no funciona para mí
Sachin

-3

Yo uso el tmp var para cambiar.

changeTheme(v) {
    let tmp = this.state.tableData
    tmp.theme = v
    this.setState({
        tableData : tmp
    })
}

2
Aquí tmp.theme = v es un estado mutante. Entonces esta no es la forma recomendada.
almoraleslopez

1
let original = {a:1}; let tmp = original; tmp.a = 5; // original is now === Object {a: 5} No hagas esto, incluso si aún no te ha dado problemas.
Chris
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.