Czy React zachowuje kolejność aktualizacji stanu?

149

Wiem, że React może przeprowadzać aktualizacje stanu asynchronicznie i wsadowo w celu optymalizacji wydajności. Dlatego nigdy nie możesz ufać, że stan zostanie zaktualizowany po wywołaniu setState. Ale można zaufać React aby zaktualizować stan w tej samej kolejności, jak setStatenazywany jest przez

  1. ten sam składnik?
  2. różne komponenty?

W poniższych przykładach rozważ kliknięcie przycisku:

1. Czy kiedykolwiek istnieje możliwość, że a jest fałszywe i b jest prawdziwe dla:

class Container extends React.Component {
  constructor(props) {
    super(props);
    this.state = { a: false, b: false };
  }

  render() {
    return <Button onClick={this.handleClick}/>
  }

  handleClick = () => {
    this.setState({ a: true });
    this.setState({ b: true });
  }
}

2. Czy kiedykolwiek istnieje możliwość, że a jest fałszywe i b jest prawdziwe dla:

class SuperContainer extends React.Component {
  constructor(props) {
    super(props);
    this.state = { a: false };
  }

  render() {
    return <Container setParentState={this.setState.bind(this)}/>
  }
}

class Container extends React.Component {
  constructor(props) {
    super(props);
    this.state = { b: false };
  }

  render() {
    return <Button onClick={this.handleClick}/>
  }

  handleClick = () => {
    this.props.setParentState({ a: true });
    this.setState({ b: true });
  }
}

Pamiętaj, że są to skrajne uproszczenia mojego przypadku użycia. Zdaję sobie sprawę, że mogę to zrobić inaczej, np. Aktualizację obu parametrów stanu w tym samym czasie w przykładzie 1, a także wykonanie aktualizacji drugiego stanu w wywołaniu zwrotnym aktualizacji pierwszego stanu w przykładzie 2. Jednak to nie jest moje pytanie, interesuje mnie tylko to, czy istnieje dobrze zdefiniowany sposób, w jaki React przeprowadza te aktualizacje stanu, nic więcej.

Każda odpowiedź poparta dokumentacją jest bardzo mile widziana.

darksmurf
źródło
3
nie wydaje się to bezsensowne pytanie, możesz również zadać to pytanie na temat problemów z github na stronie react, dan abramov jest tam zwykle bardzo pomocny. Kiedy miałem takie trudne pytania, zadawałem, a on odpowiadał. Złe jest to, że tego rodzaju problemy nie są udostępniane publicznie, tak jak w oficjalnych dokumentach (aby inni również mieli do nich łatwy dostęp).
Wydaje
Weźmy na przykład to: github.com/facebook/react/issues/11793 , uważam, że rzeczy omówione w tym numerze byłyby pomocne dla wielu programistów, ale nie ma ich w oficjalnej dokumentacji, ponieważ ludzie FB uważają to za zaawansowane. To samo dotyczy innych możliwych rzeczy. Wydaje mi się, że oficjalny artykuł zatytułowany coś w rodzaju „zarządzania państwem w głębokiej reakcji” lub „pułapki zarządzania państwem”, który omawia wszystkie narożne przypadki zarządzania państwem, jak w twoim pytaniu, nie byłby zły. może uda nam się
zmusić
W moim pytaniu jest link do świetnego artykułu na temat medium. Powinien obejmować 95% przypadków użycia w stanie. :)
Michał
2
@Michal, ale ten artykuł nadal nie odpowiada na to pytanie. IMHO
giorgim

Odpowiedzi:

352

Pracuję nad Reactem.

TLDR:

Ale czy możesz zaufać Reactowi, że zaktualizuje stan w tej samej kolejności, w jakiej jest wywoływana funkcja setState

  • ten sam składnik?

Tak.

  • różne komponenty?

Tak.

Zamówienie o aktualizacjach zawsze jest przestrzegane. To, czy widzisz stan pośredni „między” nimi, czy nie, zależy od tego, czy jesteś w grupie, czy nie.

Obecnie (React 16 i wcześniejsze) tylko aktualizacje wewnątrz obsługi zdarzeń React są domyślnie grupowane . Istnieje niestabilny interfejs API, który wymusza przetwarzanie wsadowe poza programami obsługi zdarzeń w rzadkich przypadkach, gdy jest to potrzebne.

W przyszłych wersjach (prawdopodobnie React 17 i późniejszych), React będzie domyślnie grupował wszystkie aktualizacje, więc nie będziesz musiał o tym myśleć. Jak zawsze, będziemy ogłaszać wszelkie zmiany w tym zakresie na blogu React oraz w informacjach o wydaniu.


Kluczem do zrozumienia tego jest to, że bez względu na to, ile setState()wywołań w ilu składnikach wykonujesz w module obsługi zdarzeń React , po zakończeniu zdarzenia wygenerują tylko jedno ponowne renderowanie . Ma to kluczowe znaczenie dla dobrej wydajności w dużych aplikacjach, ponieważ jeśli Childi Parentkażde wywołanie setState()podczas obsługi zdarzenia kliknięcia, nie chcesz ponownie renderować Childdwukrotnie.

W obu twoich przykładach setState()wywołania zachodzą wewnątrz procedury obsługi zdarzeń React. Dlatego zawsze są one przepłukiwane razem na koniec zdarzenia (i nie widzisz stanu pośredniego).

Aktualizacje są zawsze płytko scalane w kolejności, w jakiej występują . Więc jeśli pierwsza aktualizacja to {a: 10}, druga to {b: 20}, a trzecia to {a: 30}, renderowany stan będzie {a: 30, b: 20}. Nowsza aktualizacja tego samego klucza stanu (np. Jak aw moim przykładzie) zawsze „wygrywa”.

this.stateObiekt zostanie zaktualizowany, gdy ponownie renderować UI pod koniec partii. Jeśli więc chcesz zaktualizować stan na podstawie poprzedniego stanu (na przykład zwiększając licznik), powinieneś użyć setState(fn)wersji funkcjonalnej, która daje poprzedni stan, zamiast czytać z this.state. Jeśli jesteś ciekawy, dlaczego tak się dzieje, wyjaśniłem to szczegółowo w tym komentarzu .


W twoim przykładzie nie zobaczylibyśmy „stanu pośredniego”, ponieważ znajdujemy się w module obsługi zdarzeń React, w którym jest włączone przetwarzanie wsadowe (ponieważ React „wie”, kiedy wychodzimy z tego zdarzenia).

Jednak zarówno w React 16, jak i we wcześniejszych wersjach, nie ma domyślnego przetwarzania wsadowego poza programami obsługi zdarzeń React . Więc jeśli w twoim przykładzie zamiast procedury obsługi odpowiedzi AJAX handleClick, każdy setState()byłby przetwarzany natychmiast po tym, jak to się dzieje. W tym przypadku, tak, by zobaczyć stan pośredni:

promise.then(() => {
  // We're not in an event handler, so these are flushed separately.
  this.setState({a: true}); // Re-renders with {a: true, b: false }
  this.setState({b: true}); // Re-renders with {a: true, b: true }
  this.props.setParentState(); // Re-renders the parent
});

Zdajemy sobie sprawę, że to niewygodne, że zachowanie różni się w zależności od tego, czy jesteś w programie obsługi zdarzeń, czy nie . Zmieni się to w przyszłej wersji React, która będzie domyślnie grupować wszystkie aktualizacje (i zapewni API opt-in do synchronicznego opróżniania zmian). Dopóki nie zmienimy domyślnego zachowania (potencjalnie w Reakcie 17), istnieje API, którego możesz użyć do wymuszenia grupowania :

promise.then(() => {
  // Forces batching
  ReactDOM.unstable_batchedUpdates(() => {
    this.setState({a: true}); // Doesn't re-render yet
    this.setState({b: true}); // Doesn't re-render yet
    this.props.setParentState(); // Doesn't re-render yet
  });
  // When we exit unstable_batchedUpdates, re-renders once
});

Wszystkie wewnętrzne procedury obsługi zdarzeń React są opakowane, unstable_batchedUpdatesdlatego są domyślnie grupowane. Zwróć uwagę, że unstable_batchedUpdatesdwukrotne zawinięcie aktualizacji nie ma żadnego efektu. Aktualizacje są opróżniane, gdy wychodzimy z unstable_batchedUpdateswywołania zewnętrznego .

Ten interfejs API jest „niestabilny” w tym sensie, że usuniemy go, gdy przetwarzanie wsadowe jest już domyślnie włączone. Jednak nie usuniemy go w pomniejszej wersji, więc możesz bezpiecznie na nim polegać aż do React 17, jeśli chcesz wymusić wsadowanie w niektórych przypadkach poza programami obsługi zdarzeń React.


Podsumowując, jest to mylący temat, ponieważ React domyślnie umieszcza tylko partie wewnątrz programów obsługi zdarzeń. Zmieni się to w przyszłych wersjach i wówczas zachowanie będzie prostsze. Rozwiązaniem nie jest jednak partie mniejsze , ale domyślnie większe partie . To właśnie zamierzamy zrobić.

Dan Abramov
źródło
1
Jednym ze sposobów, aby „zawsze uzyskać właściwą kolejność”, jest utworzenie tymczasowego obiektu, przypisanie różnych wartości (np. obj.a = true; obj.b = true), A na końcu po prostu to zrobić this.setState(obj). Jest to bezpieczne niezależnie od tego, czy znajdujesz się w programie obsługi zdarzeń, czy nie. Może to być niezła sztuczka, jeśli często popełniasz błąd, ustawiając stan kilka razy poza programami obsługi zdarzeń.
Chris,
Nie możemy więc polegać na tym, że przetwarzanie wsadowe będzie ograniczone tylko do jednego modułu obsługi zdarzeń - jak jasno powiedziałeś, przynajmniej dlatego, że nie nastąpi to wkrótce. W takim razie powinniśmy użyć funkcji setState z funkcją Updater, aby uzyskać dostęp do najnowszego stanu, prawda? Ale co, jeśli potrzebuję użyć pliku state.filter, aby XHR odczytywał niektóre dane, a następnie wprowadził je do stanu? Wygląda na to, że będę musiał umieścić XHR z odroczonym wywołaniem zwrotnym (a tym samym efektem ubocznym) do aktualizatora. Czy jest to zatem uważane za najlepszą praktykę?
Maksim Gumerov
1
A tak przy okazji, oznacza to również, że nie powinniśmy w ogóle czytać z this.state; jedynym rozsądnym sposobem na odczytanie jakiegoś elementu state.X jest odczytanie go w funkcji aktualizującej z jego argumentu. Pisanie do this.state również jest niebezpieczne. Po co więc w ogóle zezwalać na dostęp do this.state? To może być samodzielne pytania, ale przede wszystkim próbuję zrozumieć, czy dobrze zrozumiałem.
Maksim Gumerov
11
ta odpowiedź powinna zostać dodana do dokumentacji actjs.org
Deen John
2
Czy mógłbyś wyjaśnić w tym poście, czy „program obsługi zdarzeń reagowania” zawiera componentDidUpdatei inne wywołania zwrotne cyklu życia od React 16? Z góry dziękuję!
Ivan
7

To właściwie dość interesujące pytanie, ale odpowiedź nie powinna być zbyt skomplikowana. Jest taki świetny artykuł na medium, który zawiera odpowiedź.

1) Jeśli to zrobisz

this.setState({ a: true });
this.setState({ b: true });

Nie sądzę, żeby doszło do sytuacji, w której abędzie truei bbędzie z falsepowodu wsadowania .

Jednak jeśli bzależy naa czym tam rzeczywiście może być sytuacja, w której nie byłoby uzyskać oczekiwany stan.

// assuming this.state = { value: 0 };
this.setState({ value: this.state.value + 1});
this.setState({ value: this.state.value + 1});
this.setState({ value: this.state.value + 1});

Po przetworzeniu wszystkich powyższych wywołań this.state.valuebędzie 1, a nie 3, jak można się spodziewać.

Jest to wspomniane w artykule: setState accepts a function as its parameter

// assuming this.state = { value: 0 };
this.setState((state) => ({ value: state.value + 1}));
this.setState((state) => ({ value: state.value + 1}));
this.setState((state) => ({ value: state.value + 1}));

To nam da this.state.value === 3

Michał
źródło
Co, jeśli this.state.valuejest aktualizowany zarówno w programach obsługi zdarzeń (gdzie setStatejest wsadowy), jak i wywołaniach zwrotnych AJAX (gdzie niesetState jest wsadowy). W programach obsługi zdarzeń użyłbym, aby zawsze mieć pewność, że aktualizuję stan przy użyciu aktualnie zaktualizowanego stanu dostarczonego przez funkcję. Czy powinienem używać funkcji aktualizującej w kodzie wywołania zwrotnego AJAX, nawet jeśli wiem, że nie jest to partia? Czy mógłbyś wyjaśnić użycie w wywołaniu zwrotnym AJAX z lub bez funkcji aktualizatora? Dziękuję Ci! updater functionsetStatesetState
tonix
@Michal, cześć Michał chciał tylko zadać zwykłe pytanie, czy to prawda, że ​​jeśli mamy this.setState ({value: 0}); this.setState ({value: this.state.value + 1}); pierwszy setState zostanie zignorowany i tylko drugi setState zostanie wykonany?
Dickens
@Dickens Wierzę, że obaj setStatezostaną straceni, ale ostatni wygra.
Michał
3

Wiele połączeń w tym samym cyklu może być połączonych razem. Na przykład, jeśli spróbujesz zwiększyć ilość towaru więcej niż raz w tym samym cyklu, da to równowartość:

Object.assign(
  previousState,
  {quantity: state.quantity + 1},
  {quantity: state.quantity + 1},
  ...
)

https://reactjs.org/docs/react-component.html

Mosè Raguzzini
źródło
3

jak w dok

setState () kolejkuje zmiany do stanu komponentu i informuje Reacta, że ​​ten komponent i jego dzieci muszą zostać ponownie wyrenderowane ze zaktualizowanym stanem. Jest to główna metoda używana do aktualizowania interfejsu użytkownika w odpowiedzi na programy obsługi zdarzeń i odpowiedzi serwera.

dokona zmiany jak w kolejce ( FIFO : First In First Out) pierwsze wywołanie będzie wykonane jako pierwsze

Ali
źródło
cześć Ali, chciałem tylko zadać zwykłe pytanie, czy to prawda, że ​​jeśli mamy this.setState ({wartość: 0}); this.setState ({value: this.state.value + 1}); pierwszy setState zostanie zignorowany i tylko drugi setState zostanie wykonany?
Dickens