this.setState führt Zustände nicht wie erwartet zusammen

159

Ich habe folgenden Zustand:

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

Dann aktualisiere ich den Status:

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

Da setStateangenommen wird, zu verschmelzen, würde ich erwarten, dass es ist:

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

Aber stattdessen frisst es die ID und der Zustand ist:

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

Ist dies das erwartete Verhalten und wie kann nur eine Eigenschaft eines verschachtelten Statusobjekts aktualisiert werden?

Pickels
quelle

Antworten:

143

Ich denke, setState()keine rekursive Zusammenführung.

Sie können den Wert des aktuellen Status verwenden this.state.selected, um einen neuen Status zu erstellen, und dann Folgendes aufrufen setState():

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

Ich habe hier die Funktionsfunktion _.extend()(aus der Bibliothek underscore.js) verwendet, um Änderungen am vorhandenen selectedTeil des Status zu verhindern, indem ich eine flache Kopie davon erstellt habe.

Eine andere Lösung wäre das Schreiben, setStateRecursively()das eine rekursive Zusammenführung eines neuen Zustands durchführt und dann damit aufruft replaceState():

setStateRecursively: function(stateUpdate, callback) {
  var newState = mergeStateRecursively(this.state, stateUpdate);
  this.replaceState(newState, callback);
}
andreypopp
quelle
7
Funktioniert dies zuverlässig, wenn Sie es mehrmals ausführen, oder besteht die Möglichkeit, dass die replaceState-Aufrufe in eine Warteschlange gestellt werden und der letzte gewinnt?
Edoloughlin
111
Für Leute, die Extend bevorzugen und die auch Babel verwenden, können Sie dies tun, this.setState({ selected: { ...this.state.selected, name: 'barfoo' } })was übersetzt wird inthis.setState({ selected: _extends({}, this.state.selected, { name: 'barfoo' }) });
Pickels
8
@Pickels das ist toll, schön idiomatisch für React und benötigt underscore/ nicht lodash. Spinnen Sie es bitte in eine eigene Antwort.
Ericsoco
14
this.state ist nicht unbedingt aktuell . Mit der Extend-Methode können Sie unerwartete Ergebnisse erzielen. Das Übergeben einer Aktualisierungsfunktion an setStateist die richtige Lösung.
Strom
1
@ EdwardD'Souza Es ist die Lodash-Bibliothek. Es wird üblicherweise als Unterstrich aufgerufen, genauso wie jQuery üblicherweise als Dollarzeichen aufgerufen wird. (Also, es ist _.extend ().)
mjk
96

Unveränderlichkeitshilfen wurden kürzlich zu React.addons hinzugefügt. Damit können Sie jetzt Folgendes tun:

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

Dokumentation der Unveränderlichkeitshelfer .

user3874611
quelle
7
Update ist veraltet. facebook.github.io/react/docs/update.html
Thedanotto
3
@thedanotto Es sieht so aus, als wäre die React.addons.update()Methode veraltet, aber die Ersatzbibliothek github.com/kolodny/immutability-helper enthält eine entsprechende update()Funktion.
Bungle
37

Da viele der Antworten den aktuellen Status als Grundlage für das Zusammenführen neuer Daten verwenden, wollte ich darauf hinweisen, dass dies nicht funktionieren kann. Statusänderungen werden in die Warteschlange gestellt und ändern das Statusobjekt einer Komponente nicht sofort. Wenn Sie auf Statusdaten verweisen, bevor die Warteschlange verarbeitet wurde, erhalten Sie veraltete Daten, die nicht die ausstehenden Änderungen widerspiegeln, die Sie in setState vorgenommen haben. Aus den Dokumenten:

setState () mutiert this.state nicht sofort, sondern erstellt einen ausstehenden Statusübergang. Der Zugriff auf this.state nach dem Aufrufen dieser Methode kann möglicherweise den vorhandenen Wert zurückgeben.

Dies bedeutet, dass die Verwendung des "aktuellen" Status als Referenz bei nachfolgenden Aufrufen von setState unzuverlässig ist. Beispielsweise:

  1. Erster Aufruf von setState, bei dem eine Änderung des Statusobjekts in die Warteschlange gestellt wird
  2. Zweiter Aufruf von setState. Ihr Status verwendet verschachtelte Objekte, sodass Sie eine Zusammenführung durchführen möchten. Bevor Sie setState aufrufen, erhalten Sie das aktuelle Statusobjekt. Dieses Objekt spiegelt keine Änderungen in der Warteschlange wider, die beim ersten Aufruf von setState oben vorgenommen wurden, da es sich immer noch um den ursprünglichen Status handelt, der jetzt als "veraltet" betrachtet werden sollte.
  3. Führen Sie eine Zusammenführung durch. Das Ergebnis ist der ursprüngliche "veraltete" Status plus neue Daten, die Sie gerade festgelegt haben. Änderungen gegenüber dem ersten Aufruf von setState werden nicht berücksichtigt. Ihr setState-Anruf stellt diese zweite Änderung in die Warteschlange.
  4. Reaktionsprozesswarteschlange. Der erste setState-Aufruf wird verarbeitet und der Status aktualisiert. Der zweite setState-Aufruf wird verarbeitet und aktualisiert den Status. Das Objekt des zweiten setState hat jetzt das erste ersetzt, und da die Daten, die Sie beim Tätigen dieses Anrufs hatten, veraltet waren, haben die geänderten veralteten Daten dieses zweiten Aufrufs die im ersten Aufruf vorgenommenen Änderungen blockiert, die verloren gehen.
  5. Wenn die Warteschlange leer ist, bestimmt React, ob gerendert werden soll usw. An diesem Punkt rendern Sie die Änderungen, die im zweiten setState-Aufruf vorgenommen wurden, und es ist, als ob der erste setState-Aufruf nie stattgefunden hätte.

Wenn Sie den aktuellen Status verwenden müssen (z. B. um Daten zu einem verschachtelten Objekt zusammenzuführen), akzeptiert setState alternativ eine Funktion als Argument anstelle eines Objekts. Die Funktion wird nach vorherigen Aktualisierungen des Status aufgerufen und übergibt den Status als Argument. Auf diese Weise können atomare Änderungen vorgenommen werden, um die vorherigen Änderungen zu berücksichtigen.

bgannonpl
quelle
5
Gibt es ein Beispiel oder mehr Dokumente dazu? afaik, niemand berücksichtigt dies.
Josef.B
@ Tennis Versuchen Sie meine Antwort unten.
Martin Dawson
Ich sehe nicht, wo das Problem liegt. Was Sie beschreiben, tritt nur auf, wenn Sie versuchen, nach dem Aufruf von setState auf den Status "new" zuzugreifen. Das ist ein ganz anderes Problem, das nichts mit der OP-Frage zu tun hat. Der Zugriff auf den alten Status vor dem Aufruf von setState, damit Sie ein neues Statusobjekt erstellen können, wäre niemals ein Problem, und genau das erwartet React.
nextgentech
@nextgentech "Der Zugriff auf den alten Status vor dem Aufruf von setState, damit Sie ein neues Statusobjekt erstellen können, wäre niemals ein Problem" - Sie irren sich. Wir sprechen über das Überschreiben des Status basierend auf this.statedem Status , der möglicherweise veraltet ist (oder besser gesagt, die Aktualisierung in der Warteschlange ansteht), wenn Sie darauf zugreifen.
Madbreaks
1
@Madbreaks Ok, ja, beim erneuten Lesen der offiziellen Dokumente sehe ich, dass angegeben ist, dass Sie die Funktionsform von setState verwenden müssen, um den Status basierend auf dem vorherigen Status zuverlässig zu aktualisieren. Trotzdem bin ich bisher noch nie auf ein Problem gestoßen, wenn ich nicht mehrmals versucht habe, setState in derselben Funktion aufzurufen. Vielen Dank für die Antworten, die mich dazu gebracht haben, die technischen Daten erneut zu lesen.
nextgentech
10

Ich wollte keine weitere Bibliothek installieren, daher hier noch eine andere Lösung.

Anstatt:

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

Tun Sie dies stattdessen:

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

Oder dank @ icc97 in den Kommentaren noch prägnanter, aber wohl weniger lesbar:

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

Um klar zu sein, verstößt diese Antwort auch nicht gegen die oben erwähnten Bedenken von @bgannonpl.

Ryan Shillington
quelle
Upvote, überprüfen. Ich frage mich nur, warum ich es nicht so mache. this.setState({ selected: Object.assign(this.state.selected, { name: "Barfoo" }));
Justus Romijn
1
@JustusRomijn Leider würden Sie dann den aktuellen Status direkt ändern. Object.assign(a, b)nimmt Attribute von b, fügt sie ein / überschreibt sie aund kehrt dann zurück a. Reagieren Sie, wenn Sie den Status direkt ändern.
Ryan Shillington
Genau. Beachten Sie nur, dass dies nur für flaches Kopieren / Zuweisen funktioniert.
Madbreaks
1
@JustusRomijn Sie können dies gemäß MDN-Zuweisung in einer Zeile tun , wenn Sie {}als erstes Argument hinzufügen : this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });. Dies mutiert nicht den ursprünglichen Zustand.
icc97
@ icc97 Schön! Das habe ich meiner Antwort hinzugefügt.
Ryan Shillington
8

Beibehaltung des vorherigen Status basierend auf der Antwort von @bgannonpl:

Lodash Beispiel:

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

Um zu überprüfen, ob es ordnungsgemäß funktioniert, können Sie den zweiten Parameterfunktionsrückruf verwenden:

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

Ich habe verwendet, mergeweil extenddie anderen Eigenschaften sonst verworfen werden.

Beispiel für die Unveränderlichkeit reagieren :

import update from "react-addons-update";

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

Meine Lösung für diese Art von Situation besteht darin, wie eine andere Antwort hervorhob , die Unveränderlichkeitshelfer zu verwenden .

Da das Einstellen des Status in der Tiefe eine häufige Situation ist, habe ich das folgende Mixin erstellt:

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

Dieses Mixin ist in den meisten meiner Komponenten enthalten und wird im Allgemeinen nicht setStatemehr direkt verwendet.

Mit diesem Mixin müssen Sie lediglich die Funktion setStateinDepthfolgendermaßen aufrufen, um den gewünschten Effekt zu erzielen :

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

Für mehr Informationen:

Dorian
quelle
Entschuldigen Sie die späte Antwort, aber Ihr Mixin-Beispiel scheint interessant zu sein. Wenn ich jedoch 2 verschachtelte Zustände habe, z. B. this.state.test.one und this.state.test.two. Ist es richtig, dass nur eine davon aktualisiert und die andere gelöscht wird? Wenn ich das erste aktualisiere, wenn das zweite im Zustand ist, wird es entfernt ...
mamruoc
hy @mamruoc, this.state.test.twowird auch nach dem Update this.state.test.onemit noch da sein setStateInDepth({test: {one: {$set: 'new-value'}}}). Ich verwende diesen Code in allen meinen Komponenten, um die eingeschränkte setStateFunktion von React zu umgehen .
Dorian
1
Ich habe die Lösung gefunden. Ich habe this.state.testals Array initiiert . Wechsel zu Objekten, gelöst.
Mamruoc
2

Ich verwende es6-Klassen, und am Ende hatte ich mehrere komplexe Objekte in meinem obersten Status und versuchte, meine Hauptkomponente modularer zu gestalten. Deshalb habe ich einen einfachen Klassen-Wrapper erstellt, um den Status in der obersten Komponente beizubehalten, aber mehr lokale Logik zuzulassen .

Die Wrapper-Klasse übernimmt eine Funktion als Konstruktor, die eine Eigenschaft für den Hauptkomponentenstatus festlegt.

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

Dann erstelle ich für jede komplexe Eigenschaft im obersten Zustand eine StateWrapped-Klasse. Sie können die Standard-Requisiten hier im Konstruktor festlegen. Sie werden festgelegt, wenn die Klasse initialisiert wird. Sie können Werte auf den lokalen Status verweisen und den lokalen Status festlegen, auf lokale Funktionen verweisen und die Kette übergeben:

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


}

Dann benötigt meine Top-Level-Komponente nur den Konstruktor, um jede Klasse auf ihre Top-Level-Statuseigenschaft, ein einfaches Rendering und alle Funktionen zu setzen, die komponentenübergreifend kommunizieren.

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

Scheint für meine Zwecke recht gut zu funktionieren, bedenken Sie jedoch, dass Sie den Status der Eigenschaften, die Sie umschlossenen Komponenten auf der obersten Komponente zuweisen, nicht ändern können, da jede umschlossene Komponente ihren eigenen Status verfolgt, aber den Status auf der obersten Komponente aktualisiert jedes Mal, wenn es sich ändert.


quelle
2

Ab sofort

Wenn der nächste Status vom vorherigen Status abhängt, empfehlen wir stattdessen die Verwendung des Updater-Funktionsformulars:

gemäß der Dokumentation https://reactjs.org/docs/react-component.html#setstate unter Verwendung von:

this.setState((prevState) => {
    return {quantity: prevState.quantity + 1};
});
egidiocs
quelle
Vielen Dank für das Zitat / den Link, der in anderen Antworten fehlte.
Madbreaks
1

Lösung

Bearbeiten: Diese Lösung verwendet die Spread-Syntax . Das Ziel war es, ein Objekt ohne Verweise darauf zu erstellen prevState, damit prevStatees nicht geändert wird. Aber in meiner Verwendung prevStateschien manchmal geändert. Für ein perfektes Klonen ohne Nebenwirkungen konvertieren wir jetzt prevStatezu JSON und dann wieder zurück. (Die Inspiration zur Verwendung von JSON kam von MDN .)

Merken:

Schritte

  1. Erstellen Sie eine Kopie der Root-Level - Eigenschaft der , statedie Sie ändern wollen
  2. Mutiere dieses neue Objekt
  3. Erstellen Sie ein Update-Objekt
  4. Geben Sie das Update zurück

Die Schritte 3 und 4 können in einer Zeile kombiniert werden.

Beispiel

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

Vereinfachtes Beispiel:

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

Dies folgt gut den React-Richtlinien. Basierend auf der Antwort von eicksl auf eine ähnliche Frage.

Tim
quelle
1

ES6-Lösung

Wir setzen den Zustand zunächst

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

Wir ändern eine Eigenschaft auf einer bestimmten Ebene des Statusobjekts:

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

Der Reaktionsstatus führt die rekursive Zusammenführung nicht durch, setStateerwartet jedoch, dass nicht gleichzeitig Aktualisierungen der Statusstatusmitglieder durchgeführt werden. Sie müssen entweder eingeschlossene Objekte / Arrays selbst kopieren (mit array.slice oder Object.assign) oder die dedizierte Bibliothek verwenden.

Wie dieser. NestedLink unterstützt direkt die Behandlung des zusammengesetzten Reaktionszustands.

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

Außerdem kann der Link zum selectedoder selected.nameüberall als einzelne Requisite weitergegeben und dort mit modifiziert werden set.

Gaperton
quelle
-2

Hast du den Ausgangszustand eingestellt?

Ich werde zum Beispiel einen Teil meines eigenen Codes verwenden:

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

In einer App, an der ich arbeite, habe ich den Status so eingestellt und verwendet. Ich glaube, setStateSie können dann einfach die gewünschten Zustände individuell bearbeiten. Ich habe es so genannt:

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

Denken Sie daran, React.createClassdass Sie den Status innerhalb der von Ihnen aufgerufenen Funktion festlegen müssengetInitialState

asherrard
quelle
1
Welches Mixin verwenden Sie in Ihrer Komponente? Das funktioniert bei mir nicht
Sachin
-3

Ich benutze die tmp var um zu ändern.

changeTheme(v) {
    let tmp = this.state.tableData
    tmp.theme = v
    this.setState({
        tableData : tmp
    })
}
hkongm
quelle
2
Hier ist tmp.theme = v ein mutierender Zustand. Dies ist also nicht der empfohlene Weg.
Almoraleslopez
1
let original = {a:1}; let tmp = original; tmp.a = 5; // original is now === Object {a: 5} Tun Sie dies nicht, auch wenn es Ihnen noch keine Probleme bereitet hat.
Chris