this.setState nie łączy stanów, jak bym się spodziewał

160

Mam następujący stan:

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

Następnie aktualizuję stan:

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

Ponieważ setStatema nastąpić scalenie, spodziewałbym się, że będzie to:

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

Ale zamiast tego zjada identyfikator, a stan to:

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

Czy to oczekiwane zachowanie i jakie jest rozwiązanie polegające na aktualizowaniu tylko jednej właściwości zagnieżdżonego obiektu stanu?

Pickels
źródło

Odpowiedzi:

143

Myślę, że setState()nie wykonuje scalania rekurencyjnego.

Możesz użyć wartości bieżącego stanu, this.state.selectedaby utworzyć nowy stan, a następnie wywołać setState()to:

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

Użyłem tutaj funkcji _.extend()funkcji (z biblioteki underscore.js), aby zapobiec modyfikowaniu istniejącej selectedczęści stanu poprzez utworzenie jej płytkiej kopii.

Innym rozwiązaniem byłoby napisanie, setStateRecursively()które dokonuje rekursywnego scalania w nowym stanie, a następnie wywołuje replaceState()z nim:

setStateRecursively: function(stateUpdate, callback) {
  var newState = mergeStateRecursively(this.state, stateUpdate);
  this.replaceState(newState, callback);
}
andreypopp
źródło
7
Czy to działa niezawodnie, jeśli zrobisz to więcej niż raz, czy jest szansa, że ​​zareaguje w kolejce do wywołań replaceState i ostatni wygra?
edoloughlin
111
Dla osób, które wolą używać rozszerzenia i że są również za pomocą Babel można zrobić this.setState({ selected: { ...this.state.selected, name: 'barfoo' } })który dostaje przeliczonych nathis.setState({ selected: _extends({}, this.state.selected, { name: 'barfoo' }) });
pickels
8
@Pickels to jest świetne, ładnie idiomatyczne dla Reacta i nie wymaga underscore/ lodash. Proszę przekształcić to we własną odpowiedź.
ericsoco
14
this.state niekoniecznie aktualne . Możesz uzyskać nieoczekiwane rezultaty używając metody rozszerzania. Przekazanie funkcji aktualizacji do setStatejest poprawnym rozwiązaniem.
Strom
1
@ EdwardD'Souza To biblioteka lodash. Jest często wywoływany jako podkreślenie, tak samo jak jQuery jest często wywoływane jako znak dolara. (Więc to jest _.extend ().)
mjk
96

Pomocnicy niezmienności zostali niedawno dodani do React.addons, więc dzięki temu możesz teraz zrobić coś takiego:

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

Dokumentacja pomocników niezmienności .

user3874611
źródło
7
aktualizacja jest przestarzała. facebook.github.io/react/docs/update.html
thedanotto
3
@thedanotto Wygląda na to, że React.addons.update()metoda jest przestarzała, ale zastępcza biblioteka github.com/kolodny/immutability-helper zawiera równoważną update()funkcję.
Bungle
37

Ponieważ wiele odpowiedzi wykorzystuje bieżący stan jako podstawę do scalania nowych danych, chciałem zwrócić uwagę, że może to się zepsuć. Zmiany stanu są umieszczane w kolejce i nie powodują natychmiastowej modyfikacji obiektu stanu składnika. Odwołanie się do danych stanu przed przetworzeniem kolejki da zatem nieaktualne dane, które nie odzwierciedlają oczekujących zmian wprowadzonych w setState. Z dokumentów:

setState () nie zmienia natychmiast this.state, ale tworzy oczekującą zmianę stanu. Dostęp do this.state po wywołaniu tej metody może potencjalnie zwrócić istniejącą wartość.

Oznacza to, że używanie „bieżącego” stanu jako odniesienia w kolejnych wywołaniach funkcji setState jest zawodne. Na przykład:

  1. Pierwsze wywołanie setState, kolejkowanie zmiany do obiektu stanu
  2. Drugie wywołanie setState. Twój stan używa obiektów zagnieżdżonych, więc chcesz wykonać scalenie. Przed wywołaniem setState otrzymujesz obiekt stanu bieżącego. Ten obiekt nie odzwierciedla umieszczonych w kolejce zmian dokonanych w pierwszym wywołaniu metody setState powyżej, ponieważ jest to nadal oryginalny stan, który powinien być teraz uważany za „nieaktualny”.
  3. Wykonaj scalanie. Rezultatem jest oryginalny „nieaktualny” stan plus nowe dane, które właśnie ustawiłeś, zmiany z początkowego wywołania setState nie są odzwierciedlane. Twoje wywołanie setState kolejkuje po tej drugiej zmianie.
  4. Reaguj kolejkę procesów. Pierwsze wywołanie setState jest przetwarzane, aktualizowanie stanu. Drugie wywołanie setState jest przetwarzane, aktualizując stan. Drugi obiekt setState zastąpił teraz pierwszy, a ponieważ dane, które miałeś podczas wykonywania tego wywołania, były nieaktualne, zmodyfikowane nieaktualne dane z tego drugiego wywołania spowodowały utratę zmian dokonanych w pierwszym wywołaniu.
  5. Gdy kolejka jest pusta, React określa, czy renderować itd. W tym momencie wyrenderujesz zmiany dokonane w drugim wywołaniu setState i będzie to tak, jakby pierwsze wywołanie setState nigdy nie miało miejsca.

Jeśli potrzebujesz użyć bieżącego stanu (np. Aby scalić dane w zagnieżdżony obiekt), setState alternatywnie akceptuje funkcję jako argument zamiast obiektu; funkcja jest wywoływana po wszelkich poprzednich aktualizacjach stanu i przekazuje stan jako argument - można to więc wykorzystać do wprowadzenia atomowych zmian gwarantujących uwzględnienie poprzednich zmian.

bgannonpl
źródło
5
Czy jest przykład lub więcej dokumentów, jak to zrobić? afaik, nikt nie bierze tego pod uwagę.
Josef.B
@Dennis Wypróbuj moją odpowiedź poniżej.
Martin Dawson
Nie wiem, gdzie jest problem. To, co opisujesz, wystąpiłoby tylko wtedy, gdybyś spróbował uzyskać dostęp do „nowego” stanu po wywołaniu setState. To zupełnie inny problem, który nie ma nic wspólnego z pytaniem o PO. Uzyskanie dostępu do starego stanu przed wywołaniem setState, aby można było skonstruować nowy obiekt stanu, nigdy nie stanowiłoby problemu, a właściwie tego właśnie oczekuje React.
nextgentech
@nextgentech "Uzyskanie dostępu do starego stanu przed wywołaniem setState, aby można było skonstruować nowy obiekt stanu, nigdy nie byłoby problemem" - mylisz się. Mówimy o nadpisywaniu stanu na podstawie tego this.state, który może być nieaktualny (lub raczej oczekująca aktualizacja w kolejce), gdy uzyskujesz do niego dostęp.
Madbreaks
1
@Madbreaks Ok, tak, po ponownym przeczytaniu oficjalnych dokumentów widzę, że określono, że należy użyć postaci funkcji setState, aby niezawodnie zaktualizować stan na podstawie poprzedniego stanu. To powiedziawszy, do tej pory nigdy nie napotkałem problemu, jeśli nie próbowałem wywołać setState wiele razy w tej samej funkcji. Dzięki za odpowiedzi, które skłoniły mnie do ponownego przeczytania specyfikacji.
nextgentech
10

Nie chciałem instalować kolejnej biblioteki, więc oto kolejne rozwiązanie.

Zamiast:

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

Zrób to zamiast tego:

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

Lub, dzięki @ icc97 w komentarzach, jeszcze bardziej zwięźle, ale prawdopodobnie mniej czytelnie:

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

Ponadto, aby było jasne, ta odpowiedź nie narusza żadnego z obaw, o których @bgannonpl wspomniał powyżej.

Ryan Shillington
źródło
Głosuj za, sprawdź. Zastanawiasz się, dlaczego nie zrobić tego w ten sposób? this.setState({ selected: Object.assign(this.state.selected, { name: "Barfoo" }));
Justus Romijn
1
@JustusRomijn Niestety wtedy bezpośrednio modyfikowałbyś aktualny stan. Object.assign(a, b)pobiera atrybuty z b, wstawia / nadpisuje je ai zwraca a. Reaguj, aby ludzie byli zarozumiali, jeśli bezpośrednio zmodyfikujesz stan.
Ryan Shillington
Dokładnie. Zwróć uwagę, że działa to tylko w przypadku płytkiego kopiowania / przypisywania.
Madbreaks
1
@JustusRomijn Można to zrobić zgodnie MDN Przypisanie w jednej linii, jeśli dodać {}jako pierwszy argument: this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });. Nie zmienia to pierwotnego stanu.
icc97,
@ icc97 Świetnie! Dodałem to do mojej odpowiedzi.
Ryan Shillington,
8

Zachowanie poprzedniego stanu na podstawie odpowiedzi @bgannonpl:

Przykład Lodash :

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

Aby sprawdzić, czy działa poprawnie, możesz użyć wywołania zwrotnego funkcji drugiego parametru:

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

Użyłem, mergeponieważ extendw przeciwnym razie odrzuca inne właściwości.

Przykład niezmienności reakcji :

import update from "react-addons-update";

this.setState((previousState) => update(previousState, {
    selected:
    { 
        name: {$set: "Barfood"}
    }
});
Martin Dawson
źródło
2

Moim rozwiązaniem tego rodzaju sytuacji jest użycie, tak jak wskazała inna odpowiedź, pomocników niezmienności .

Ponieważ ustawienie stanu dogłębnego jest powszechną sytuacją, stworzyłem następującą mieszankę:

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

Ta mieszanka jest zawarta w większości moich składników i generalnie nie używam setStatejuż bezpośrednio.

Dzięki takiemu miksowi wszystko, co musisz zrobić, aby osiągnąć pożądany efekt, to wywołać funkcję setStateinDepthw następujący sposób:

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

Po więcej informacji:

  • Aby dowiedzieć się, jak działają miksy w Reakcie, zobacz oficjalną dokumentację
  • Na składni przekazanego parametru, aby setStateinDepthzobaczyć dokumentację Immutability Helpers .
dorycki
źródło
Przepraszamy za spóźnioną odpowiedź, ale Twój przykład Mixin wydaje się interesujący. Jeśli jednak mam 2 zagnieżdżone stany, np. This.state.test.one i this.state.test.two. Czy to prawda, że ​​tylko jeden z nich zostanie zaktualizowany, a drugi zostanie usunięty? Kiedy zaktualizuję pierwszy, jeśli drugi jest w stanie, zostanie usunięty ...
mamruoc
hy @mamruoc, this.state.test.twobędzie tam nadal po aktualizacji this.state.test.oneza pomocą setStateInDepth({test: {one: {$set: 'new-value'}}}). Używam tego kodu we wszystkich moich komponentach, aby obejść ograniczoną setStatefunkcję Reacta .
Dorian
1
Znalazłem rozwiązanie. Zainicjowałem this.state.testjako Array. Zmiana na Obiekty, rozwiązana.
mamruoc
2

Używam klas es6 i skończyłem z kilkoma złożonymi obiektami na moim najwyższym stanie i próbowałem uczynić mój główny komponent bardziej modułowym, więc stworzyłem prostą otokę klasy, aby zachować stan na najwyższym komponencie, ale pozwolić na bardziej lokalną logikę .

Klasa opakowująca przyjmuje funkcję jako konstruktor, która ustawia właściwość stanu głównego składnika.

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

Następnie dla każdej złożonej właściwości w stanie głównym tworzę jedną klasę StateWrapped. Możesz tutaj ustawić domyślne właściwości w konstruktorze, które zostaną ustawione, gdy klasa zostanie zainicjowana, możesz odwołać się do stanu lokalnego w celu uzyskania wartości i ustawić stan lokalny, odwołać się do funkcji lokalnych i przekazać je do łańcucha:

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


}

Więc mój komponent najwyższego poziomu potrzebuje tylko konstruktora, aby ustawić każdą klasę na jej właściwość stanu najwyższego poziomu, proste renderowanie i wszelkie funkcje, które komunikują się między komponentami.

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

Wydaje się, że do moich celów działa całkiem nieźle, pamiętaj, że nie możesz zmienić stanu właściwości przypisanych do opakowanych komponentów w komponencie najwyższego poziomu, ponieważ każdy opakowany komponent śledzi swój własny stan, ale aktualizuje stan w górnym komponencie za każdym razem, gdy się zmienia.


źródło
2

W tej chwili,

Jeśli następny stan zależy od poprzedniego, zalecamy zamiast tego użyć formularza funkcji aktualizatora:

zgodnie z dokumentacją https://reactjs.org/docs/react-component.html#setstate , używając:

this.setState((prevState) => {
    return {quantity: prevState.quantity + 1};
});
egidiocs
źródło
Dzięki za cytat / link, którego brakowało w innych odpowiedziach.
Madbreaks
1

Rozwiązanie

Edycja: To rozwiązanie używane do używania składni rozkładów . Celem było stworzenie obiektu bez żadnych odniesień prevState, aby prevStatenie był modyfikowany. Ale w moim użyciu prevStateczasami wydawał się modyfikowany. Tak więc, aby uzyskać doskonałe klonowanie bez skutków ubocznych, teraz konwertujemy prevStatena JSON, a potem z powrotem. (Inspiracja do korzystania z formatu JSON pochodzi z MDN ).

Zapamiętaj:

Kroki

  1. Zrób kopię własności korzeń szczebla o statektóre chcesz zmienić
  2. Zmutuj ten nowy obiekt
  3. Utwórz obiekt aktualizacji
  4. Zwróć aktualizację

Kroki 3 i 4 można łączyć w jednej linii.

Przykład

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

Uproszczony przykład:

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

Jest to zgodne z wytycznymi React. Na podstawie odpowiedzi Eicksla na podobne pytanie.

Tim
źródło
1

Rozwiązanie ES6

Stan początkowo ustawiamy

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

Zmieniamy właściwość na pewnym poziomie obiektu stanu:

const { selected: _selected } = this.state
const  selected = { ..._selected, name: 'Barfoo' }
this.setState({selected})
//this.state: { selected: { id: 1, name: 'Barfoo' } }
rzymski
źródło
0

Stan React nie wykonuje scalania rekurencyjnego, setStatepodczas gdy oczekuje, że w tym samym czasie nie będą dostępne lokalne aktualizacje elementu członkowskiego. Musisz albo samodzielnie skopiować zamknięte obiekty / tablice (za pomocą array.slice lub Object. assign), albo skorzystaj z dedykowanej biblioteki.

Jak ten. NestedLink bezpośrednio obsługuje obsługę złożonego stanu reakcji .

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

Ponadto łącze do selectedlub selected.namemożna przekazać wszędzie jako pojedynczy element i zmodyfikować tam za pomocą set.

gaperton
źródło
-2

czy ustawiłeś stan początkowy?

Użyję własnego kodu, na przykład:

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

W aplikacji, nad którą pracuję, tak ustawiam i używam stanu. Wierzę, setStateże możesz wtedy po prostu edytować dowolne stany indywidualnie.Nazywałem to tak:

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

Pamiętaj, że musisz ustawić stan w React.createClasswywołanej funkcjigetInitialState

asherrard
źródło
1
jakiej mieszanki używasz w swoim komponencie? To nie działa dla mnie
Sachin
-3

Używam tmp var do zmiany.

changeTheme(v) {
    let tmp = this.state.tableData
    tmp.theme = v
    this.setState({
        tableData : tmp
    })
}
hkongm
źródło
2
Tutaj tmp.theme = v jest stanem mutacji. Więc to nie jest zalecany sposób.
almoraleslopez
1
let original = {a:1}; let tmp = original; tmp.a = 5; // original is now === Object {a: 5} Nie rób tego, nawet jeśli nie spowodowało to jeszcze problemów.
Chris