this.setState non sta unendo gli stati come mi aspetterei


160

Ho il seguente stato:

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

Quindi aggiorno lo stato:

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

Dal momento che setStatesi suppone di unire, mi aspetto che sia:

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

Ma invece mangia l'id e lo stato è:

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

Questo comportamento è previsto e qual è la soluzione per aggiornare solo una proprietà di un oggetto stato nidificato?

Risposte:


143

Penso che setState()non si fonda in modo ricorsivo.

È possibile utilizzare il valore dello stato corrente this.state.selectedper costruire un nuovo stato e quindi richiamarlo setState():

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

Ho usato la funzione di _.extend()funzione (dalla libreria underscore.js) qui per impedire la modifica alla selectedparte esistente dello stato creando una copia superficiale di esso.

Un'altra soluzione sarebbe quella di scrivere setStateRecursively()che si fonde ricorsivamente su un nuovo stato e quindi chiama replaceState()con esso:

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

7
Funziona in modo affidabile se lo fai più di una volta o c'è una possibilità che la coda metterà in coda le chiamate di sostituzione dello stato e l'ultima vincerà?
edoloughlin,

111
Per le persone che preferiscono usare extension e che usano anche babel puoi fare ciò this.setState({ selected: { ...this.state.selected, name: 'barfoo' } })che viene tradotto inthis.setState({ selected: _extends({}, this.state.selected, { name: 'barfoo' }) });
Pickels

8
@Pickels questo è fantastico, piacevolmente idiomatico per React e non richiede underscore/ lodash. Spin via nella sua risposta, per favore.
ericsoco,

14
this.state non è necessariamente aggiornato . È possibile ottenere risultati imprevisti utilizzando il metodo di estensione. Passare una funzione di aggiornamento a setStateè la soluzione corretta.
Strom,

1
@ EdwardD'Souza È la libreria lodash. Viene comunemente invocato come carattere di sottolineatura, allo stesso modo in cui jQuery viene comunemente invocato come segno di dollari. (Quindi, è _.extend ().)
mjk

96

Gli helper per l'immutabilità sono stati recentemente aggiunti a React.addons, quindi ora puoi fare qualcosa del tipo:

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

Documentazione sugli aiutanti dell'immutabilità .


7
l'aggiornamento è obsoleto. facebook.github.io/react/docs/update.html
thedanotto

3
@thedanotto Sembra che il React.addons.update()metodo sia obsoleto, ma la libreria sostitutiva github.com/kolodny/immutability-helper contiene una update()funzione equivalente .
Bungle

37

Dal momento che molte delle risposte utilizzano lo stato corrente come base per l'unione di nuovi dati, ho voluto sottolineare che ciò può rompersi. Le modifiche allo stato sono in coda e non modificano immediatamente l'oggetto stato di un componente. Fare riferimento ai dati sullo stato prima che la coda sia stata elaborata fornirà quindi dati non aggiornati che non riflettono le modifiche in sospeso apportate in setState. Dai documenti:

setState () non muta immediatamente this.state ma crea una transizione di stato in sospeso. L'accesso a this.state dopo aver chiamato questo metodo può potenzialmente restituire il valore esistente.

Ciò significa che l'utilizzo dello stato "corrente" come riferimento nelle successive chiamate a setState non è affidabile. Per esempio:

  1. Prima chiamata a setState, accodando una modifica all'oggetto stato
  2. Seconda chiamata a setState. Il tuo stato utilizza oggetti nidificati, quindi desideri eseguire un'unione. Prima di chiamare setState, si ottiene l'oggetto stato corrente. Questo oggetto non riflette le modifiche in coda apportate nella prima chiamata a setState, sopra, perché è ancora lo stato originale, che ora dovrebbe essere considerato "non aggiornato".
  3. Esegui unione. Il risultato è lo stato "vecchio" originale più i nuovi dati appena impostati, le modifiche dalla chiamata iniziale setState non vengono riflesse. Le chiamate setState accodano questa seconda modifica.
  4. Coda dei processi di reazione. La prima chiamata setState viene elaborata, aggiornando lo stato. La seconda chiamata setState viene elaborata, aggiornando lo stato. L'oggetto del secondo setState ora ha sostituito il primo e poiché i dati che avevi durante l'esecuzione di quella chiamata erano obsoleti, i dati non aggiornati modificati di questa seconda chiamata hanno bloccato le modifiche apportate nella prima chiamata, che vengono perse.
  5. Quando la coda è vuota, React determina se eseguire il rendering, ecc. A questo punto verranno visualizzate le modifiche apportate nella seconda chiamata setState e sarà come se la prima chiamata setState non fosse mai avvenuta.

Se è necessario utilizzare lo stato corrente (ad esempio per unire i dati in un oggetto nidificato), setState accetta alternativamente una funzione come argomento anziché come oggetto; la funzione viene chiamata dopo qualsiasi aggiornamento precedente per dichiarare e passa lo stato come argomento, quindi può essere utilizzato per apportare modifiche atomiche garantite per rispettare le modifiche precedenti.


5
C'è un esempio o più documenti su come fare questo? in fin dei conti, nessuno ne tiene conto.
Josef.B,

@Dennis Prova la mia risposta qui sotto.
Martin Dawson,

Non vedo dove sia il problema. Quello che stai descrivendo avverrebbe solo se provi ad accedere al "nuovo" stato dopo aver chiamato setState. Questo è un problema completamente diverso che non ha nulla a che fare con la domanda OP. Accedere al vecchio stato prima di chiamare setState in modo da poter costruire un nuovo oggetto stato non sarebbe mai un problema, e in effetti è esattamente ciò che React si aspetta.
nextgentech,

@nextgentech "Accedere al vecchio stato prima di chiamare setState in modo da poter costruire un nuovo oggetto stato non sarebbe mai un problema" - ti sbagli. Stiamo parlando di sovrascrivere lo stato in base a this.state, che può essere obsoleto (o meglio, in attesa di aggiornamento in coda) quando si accede ad esso.
Madbreaks

1
@Madbreaks Ok, sì, rileggendo i documenti ufficiali vedo che è specificato che è necessario utilizzare il modulo funzione di setState per aggiornare in modo affidabile lo stato in base allo stato precedente. Detto questo, non ho mai riscontrato un problema fino ad oggi se non cercavo di chiamare setState più volte nella stessa funzione. Grazie per le risposte che mi hanno permesso di rileggere le specifiche.
nextgentech,

10

Non volevo installare un'altra libreria, quindi ecco l'ennesima soluzione.

Invece di:

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

Fallo invece:

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

Oppure, grazie a @ icc97 nei commenti, ancora più succintamente ma probabilmente meno leggibile:

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

Inoltre, per essere chiari, questa risposta non viola nessuna delle preoccupazioni che @bgannonpl ha menzionato sopra.


Vota, controlla. Mi chiedo solo perché non farlo in questo modo? this.setState({ selected: Object.assign(this.state.selected, { name: "Barfoo" }));
Justus Romijn

1
@JustusRomijn Purtroppo allora modificheresti direttamente lo stato attuale. Object.assign(a, b)prende gli attributi da b, li inserisce / li sovrascrive ae poi ritorna a. Le persone che reagiscono diventano utili se si modifica direttamente lo stato.
Ryan Shillington,

Esattamente. Basta notare che questo funziona solo per copia / assegnazione superficiale.
Madbreaks

1
@JustusRomijn È possibile farlo come da MDN assegnare in una sola riga se si aggiunge {}come primo argomento: this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });. Questo non muta lo stato originale.
icc97,

@ icc97 Nice! L'ho aggiunto alla mia risposta.
Ryan Shillington,

8

Preservare lo stato precedente in base alla risposta @bgannonpl:

Esempio di Lodash :

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

Per verificare che funzioni correttamente, è possibile utilizzare il secondo parametro funzione callback:

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

Ho usato mergeperché extendscarta altrimenti le altre proprietà.

Reagire Esempio di immutabilità :

import update from "react-addons-update";

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

2

La mia soluzione per questo tipo di situazione è quella di utilizzare, come ha sottolineato un'altra risposta, gli aiutanti dell'immutabilità .

Poiché l'impostazione dello stato in profondità è una situazione comune, ho creato il seguente mixin:

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

Questo mixin è incluso nella maggior parte dei miei componenti e generalmente non lo uso setStatepiù direttamente.

Con questo mixin, tutto ciò che devi fare per ottenere l'effetto desiderato è chiamare la funzione setStateinDepthnel modo seguente:

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

Per maggiori informazioni:


Ci scusiamo per la risposta tardiva, ma il tuo esempio di Mixin sembra interessante. Tuttavia, se ho 2 stati nidificati, ad esempio this.state.test.one e this.state.test.two. È corretto che solo uno di questi verrà aggiornato e l'altro verrà eliminato? Quando aggiorno il primo, se il secondo è nello stato, verrà rimosso ...
mamruoc,

hy @mamruoc, this.state.test.twosarà ancora lì dopo l'aggiornamento this.state.test.oneusando setStateInDepth({test: {one: {$set: 'new-value'}}}). Uso questo codice in tutti i miei componenti per aggirare la setStatefunzione limitata di React .
Dorian,

1
Ho trovato la soluzione Ho iniziato this.state.testcome un array. Cambiando in Oggetti, risolto.
mamruoc,

2

Sto usando le classi es6 e ho finito con diversi oggetti complessi sul mio stato superiore e stavo cercando di rendere il mio componente principale più modulare, quindi ho creato un semplice wrapper di classe per mantenere lo stato sul componente superiore ma consentire più logica locale .

La classe wrapper accetta una funzione come costruttore che imposta una proprietà sullo stato del componente principale.

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

Quindi, per ogni proprietà complessa sullo stato superiore, creo una classe StateWrapped. Qui puoi impostare gli oggetti di scena predefiniti nel costruttore e verranno impostati quando la classe viene inizializzata, puoi fare riferimento allo stato locale per i valori e impostare lo stato locale, fare riferimento alle funzioni locali e far passare la catena:

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


}

Quindi il mio componente di primo livello ha solo bisogno del costruttore per impostare ogni classe sulla sua proprietà di stato di livello superiore, un semplice rendering e tutte le funzioni che comunicano tra i componenti.

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!"})
}

Sembra funzionare abbastanza bene per i miei scopi, tieni presente che non puoi cambiare lo stato delle proprietà che assegni ai componenti avvolti al componente di livello superiore poiché ogni componente avvolto sta monitorando il proprio stato ma aggiornando lo stato sul componente superiore ogni volta che cambia.


2

A partire da ora,

Se lo stato successivo dipende dallo stato precedente, si consiglia invece di utilizzare il modulo della funzione di aggiornamento:

secondo la documentazione https://reactjs.org/docs/react-component.html#setstate , utilizzando:

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

Grazie per la citazione / link, che mancava da altre risposte.
Madbreaks

1

Soluzione

Modifica: questa soluzione utilizzava la sintassi diffusa . L'obiettivo era creare un oggetto senza riferimenti prevState, in modo che prevStatenon venisse modificato. Ma nel mio uso, prevStatesembrava essere modificato a volte. Quindi, per una clonazione perfetta senza effetti collaterali, ora convertiamo prevStatein JSON e poi di nuovo indietro. (L'ispirazione per usare JSON proveniva da MDN .)

Ricorda:

passi

  1. Crea una copia della proprietà a livello di root distate che si desidera modificare
  2. Muta questo nuovo oggetto
  3. Crea un oggetto di aggiornamento
  4. Restituisce l'aggiornamento

I passaggi 3 e 4 possono essere combinati su una riga.

Esempio

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

Esempio semplificato:

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

Questo segue bene le linee guida di React. Basato sulla risposta di eicksl a una domanda simile.


1

Soluzione ES6

Inizialmente impostiamo lo stato

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

Stiamo modificando una proprietà su un livello dell'oggetto stato:

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

0

Lo stato di reazione non esegue l'unione ricorsiva setState mentre si aspetta che non ci siano aggiornamenti sul posto degli stati contemporaneamente. Devi copiare tu stesso oggetti / array racchiusi (con array.slice o Object.assign) o utilizzare la libreria dedicata.

Come questo. NestedLink supporta direttamente la gestione dello stato composto React.

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

Inoltre, il collegamento al selectedo selected.namepuò essere passato ovunque come un singolo prop e modificato lì set.


-2

hai impostato lo stato iniziale?

Userò un po 'del mio codice per esempio:

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

In un'app su cui sto lavorando, ecco come ho impostato e usato lo stato. Credo setStateche puoi quindi modificare qualsiasi stato tu voglia individualmente, l'ho chiamato così:

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

Tieni presente che devi impostare lo stato all'interno della React.createClassfunzione che hai chiamatogetInitialState


1
quale mixin stai usando nel tuo componente? Questo non funziona per me
Sachin

-3

Uso il tmp var per cambiare.

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

2
Qui tmp.theme = v è stato mutante. Quindi questo non è il modo raccomandato.
almoraleslopez,

1
let original = {a:1}; let tmp = original; tmp.a = 5; // original is now === Object {a: 5} Non farlo, anche se non ti ha ancora dato problemi.
Chris,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.