technologia, cyberprzestrzeń, programowanie i koncepcja ludzi - haker w zestawie słuchawkowym i okularach z klawiaturą komputera pc nad wirtualnymi projekcjami

Dlaczego setState w React jest asynchroniczne?

11 min. czytania

setState (oraz useState) w React jest asynchroniczne, ponieważ React nie aktualizuje stanu od razu, tylko planuje tę zmianę i często łączy wiele aktualizacji w jedną, aby poprawić wydajność, nie blokować interfejsu i zachować spójność danych w komponentach. Nie możesz zakładać, że zaraz po wywołaniu setState odczytasz nową wartość – bezpośrednio po wywołaniu zobaczysz jeszcze stary stan.

Poniżej znajdziesz szczegółowe wyjaśnienie, jak to działa, dlaczego tak jest, czym ta „asynchroniczność” różni się od async/await, jakie ma konsekwencje dla kodu i dostępności (a11y) oraz jak pisać komponenty, które dobrze współpracują z takim modelem.

Co to znaczy, że setState jest „asynchroniczne”?

React w oficjalnej dokumentacji po polsku mówi wprost:

„Wywołania funkcji setState są asynchroniczne – nie spodziewaj się, że this.state będzie odzwierciedlać aktualny stan natychmiast po wywołaniu setState.”

Podobnie w artykułach o stanie:

„Wywołanie setState nie zmienia stanu od razu, tylko dopiero po jakimś czasie.”

W praktyce oznacza to:

  • kiedy wywołujesz setState / funkcję z useState, stan nie zmienia się natychmiast,
  • React dodaje aktualizację do kolejki i wykonuje ją później, najczęściej po zakończeniu obsługi zdarzenia (np. kliknięcia),
  • kod, który uruchamia się bezpośrednio po wywołaniu setState, nadal widzi stary stan.

Dlatego konstrukcja:

setCount(count + 1);
console.log(count); // nadal stara wartość!

wyświetli starą wartość count, co bywa zaskoczeniem dla osób przychodzących z jQuery czy „gołego” JS.

setState jest asynchroniczne, ale nie jest to async/await

Wielu programistów, słysząc „asynchroniczne”, myśli o async / await i obietnicach (Promise). W React to co innego:

  • sama funkcja setState jest wywoływana synchronicznie – to normalne wywołanie funkcji w JS,
  • to efekt tego wywołania (aktualizacja stanu i render) jest odłożony w czasie i wykonywany później,
  • setState nie zwraca Promise, więc nie można poprawnie napisać await setState(...).

Dlatego:

await setCount(1); // to NIE działa tak, jak można by się spodziewać

Nie dostaniesz błędu składni, ale nie poczekasz w ten sposób na aktualizację stanu, bo setState po prostu nie jest obiektem Promise.

Dobrze oddaje to porównanie z artykułu:

„Używanie setState() to w zasadzie umawianie się na termin aktualizacji stanu komponentu. Mówię 'umawianie się’, ponieważ setState() jest asynchroniczne.”

Czyli: rezerwujesz wizytę u lekarza, ale nie wychodzisz z gabinetu zdrowszy sekundę po zapisaniu się na listę.

Dlaczego React robi to asynchronicznie? Kluczowe powody

Wydajność i batching (łączenie aktualizacji)

React dokumentuje, że setState tworzy plan aktualizacji stanu, a nie zmienia go od razu. Powód numer jeden: wydajność.

Najważniejsze fakty dotyczące kosztów renderowania:

  • zmiana stanu zawsze prowadzi do ponownego renderowania komponentu,
  • render i aktualizacja DOM mogą być kosztowne, szczególnie w większych aplikacjach,
  • gdyby każda zmiana stanu natychmiast wymuszała render, przeglądarka mogłaby stać się nieodpowiedzialna / „przytykać się”.

W praktyce React kolejkue wiele wywołań setState w jednym cyklu (np. podczas jednego kliknięcia) i łączy je w jeden render, zamiast wykonywać wiele kosztownych renderów po kolei.

Stack Overflow podsumowuje to tak:

„Akcje setState są asynchroniczne i są łączone (batchowane) w celu zwiększenia wydajności.”

Oraz:

„To dlatego, że setState zmienia stan i powoduje ponowne renderowanie. To może być kosztowna operacja, a uczynienie jej synchroniczną mogłoby sprawić, że przeglądarka stanie się nieodpowiedzialna. Dlatego wywołania setState są asynchroniczne i batchowane dla lepszej wydajności UI.”

W efekcie, w takim scenariuszu:

// w jednym handlerze:
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);

nie dostaniesz czterech renderów z rzędu, tylko jedno zbiorcze podbicie stanu (pamiętaj o funkcjonalnej wersji setState – niżej).

Responsywność UI (nieblokowanie głównego wątku)

Kolejny powód: nie blokować użytkownikowi interfejsu.

Reactowe setState wiąże się z następującymi etapami:

  • przeliczeniem drzewa komponentów,
  • porównaniem wirtualnego DOM z poprzednią wersją,
  • aktualizacją rzeczywistego DOM.

Gdyby te kroki były wykonywane natychmiast po każdym setState, groziłoby to „zacięciami” przy szybkich interakcjach oraz opóźnioną reakcją na klawiaturę, mysz czy czytniki ekranu.

Dlatego React tak planuje aktualizacje, by UI pozostał możliwie płynny i responsywny. To ma bezpośredni wpływ na dostępność – osoby korzystające z klawiatury czy technologii asystujących są szczególnie wrażliwe na opóźnienia i przycięcia interfejsu.

Spójność danych – props vs state

Dokumentacja React po polsku podaje jeszcze dwa powody, dla których aktualizacje są asynchroniczne i zbatchowane:

React celowo „czeka”, aż wszystkie komponenty wywołają setState() w swoich procedurach obsługi zdarzeń, zanim zacznie ponownie renderować drzewo komponentów. Dzięki temu unikamy niepotrzebnych ponownych renderowań, co korzystnie wpływa na wydajność aplikacji.

Dodatkowo: próba wymuszenia natychmiastowej synchronizacji mogłaby przerwać spójność props vs state i utrudnić rozwój mechanizmów takich jak priorytety aktualizacji czy zaawansowany scheduling.

Innymi słowy: gdyby React musiał aktualizować stan natychmiast i renderować w każdej sytuacji, trudniej byłoby zachować przewidywalną kolejność aktualizacji w całym drzewie i wprowadzać funkcje pokroju wstrzymywania renderu.

Jak działa ta asynchroniczność „pod spodem”?

Z punktu widzenia JavaScriptu setState jest zwykłą funkcją (wywołuje się od razu), ale wewnętrznie React rejestruje aktualizację w kolejce i nie dotyka jeszcze stanu komponentu.

W efekcie, taki handler:

function handleClick() {
  setCount(count + 1);
  console.log('Po setState, count =', count); // stara wartość
}

W momencie wywołania console.log React jeszcze nie zastosował nowego stanu, a funkcja handleClick nadal „widzi” wartości z chwili wywołania (closure).

Dopiero kiedy zakończy się obsługa zdarzenia i React zbierze wszystkie wywołania setState, następuje:

  1. Obliczenie nowego stanu (na bazie starego i wszystkich aktualizacji).
  2. Ponowne wyrenderowanie komponentu (i jego dzieci).
  3. Aktualizacja DOM.

To właśnie to kontrolowane opóźnienie sprawia, że mówimy, iż setState jest „asynchroniczne” – to mechanizm batchowania i harmonogramowania aktualizacji, a nie Promise czy setTimeout w samej implementacji.

Kiedy setState jest asynchroniczne, a kiedy może wyglądać „synchronicznie”?

Zachowanie zależy od kontekstu wywołania – miejsca i sposobu, w jaki wołasz setState.

Asynchroniczne w zdarzeniach i lifecycle (klasy)

W procedurach obsługi zdarzeń Reacta (onClick, onChange) oraz w metodach cyklu życia komponentów klasowych (componentDidMount, componentDidUpdate itd.) aktualizacje są batchowane i efekt widać dopiero po zakończeniu całego cyklu. To najczęstszy przypadek w aplikacjach React, dlatego mówi się skrótowo: „setState jest asynchroniczne”.

(Historycznie) bardziej „synchroniczne” w natywnych eventach i setTimeout

W starszych analizach Reacta zwracano uwagę, że w natywnych listenerach (addEventListener) oraz wewnątrz setTimeout, setInterval lub Promise React nie zawsze stosował to samo batchowanie, więc aktualizacje mogły być widoczne „od razu” po setState (jako osobny cykl). Różne wersje Reacta zmieniały szczegóły, ale zasada projektowa pozostaje ta sama:

„Nigdy nie zakładaj, że setState jest na pewno natychmiastowe – traktuj je jako plan aktualizacji, a nie natychmiastową zmianę wartości.”

Praktyczne konsekwencje – typowe błędy i dobre wzorce

Błąd: odczyt stanu zaraz po setState

Przykład dla klas:

this.setState({ counter: this.state.counter + 1 });
console.log(this.state.counter); // wciąż stara wartość!

Przykład dla hooków:

setCounter(counter + 1);
console.log(counter); // też stara wartość

Rozwiązania:

  • korzystaj z funkcjonalnej formy – gdy nowy stan zależy od poprzedniego, użyj prev => ...,
  • callback setState w klasach – drugi argument wywoła się po aktualizacji i renderze,
  • useEffect w komponentach funkcyjnych – reaguj po renderze z nowym stanem.

Funkcjonalna forma setState / useState

Jeżeli nowy stan zależy od poprzedniego, nie rób tak:

setCounter(counter + 1); // potencjalnie niepoprawne przy wielu wywołaniach

Tylko tak:

setCounter(prevCounter => prevCounter + 1);

Funkcja aktualizująca zawsze dostaje aktualną wartość stanu z momentu zastosowania aktualizacji – niezależnie od batchowania i kolejności wywołań.

Callback po setState (komponenty klasowe)

W klasach możesz przekazać drugim argumentem funkcję, która zostanie wywołana po zaktualizowaniu stanu:

this.setState(
  { counter: this.state.counter + 1 },
  () => {
    console.log('Zaktualizowany counter:', this.state.counter);
  }
);

Dokumentacja podkreśla:

„Jeśli jako drugi argument przekażesz funkcję to zostanie ona wywołana w momencie, gdy stan będzie już zaktualizowany.”

Callback odpala się po aktualizacji komponentu, podobnie jak componentDidUpdate.

useEffect jako „miejsce na reakcję po aktualizacji”

W komponentach funkcyjnych nie ma callbacka do setState, ale tę rolę pełni useEffect:

const [open, setOpen] = useState(false);

useEffect(() => {
  if (open) {
    console.log('Stan "open" jest już TRUE, DOM po update');
    // tu możesz np. ustawić fokus
  }
}, [open]);

useEffect uruchamia się po renderze, z aktualnymi wartościami stanu i propsów, więc to idealne miejsce na akcje wymagające pewności, że stan i DOM są zaktualizowane.

Asynchroniczne setState a dostępność (a11y)

Płynność UI = lepsza dostępność

Asynchroniczne, batchowane aktualizacje chronią UI przed częstymi, ciężkimi re-renderami i pomagają utrzymać responsywność interfejsu – brak „lagów” przy wpisywaniu tekstu, przesuwaniu focusu czy reakcji na skróty klawiaturowe.

W kontekście a11y pamiętaj o trzech zależnościach:

  • czytniki ekranu reagują na zmiany w DOM,
  • użytkownicy klawiatury polegają na szybkim i przewidywalnym przesuwaniu focusu,
  • opóźnienia i „przycinanie” interfejsu realnie utrudniają korzystanie z aplikacji.

Focus po zmianie stanu (np. otwieranie modala)

Częsty scenariusz: ustawiasz stan isModalOpen na true, a po otwarciu chcesz ustawić fokus na nagłówku lub pierwszym polu formularza. Nie rób tego od razu po setIsModalOpen(true):

setIsModalOpen(true);
document.getElementById('modal-title').focus(); // ryzykowne – elementu może jeszcze nie być

Lepszy wzorzec (z poszanowaniem asynchroniczności setState):

const [isModalOpen, setIsModalOpen] = useState(false);
const titleRef = useRef(null);

useEffect(() => {
  if (isModalOpen && titleRef.current) {
    titleRef.current.focus();
  }
}, [isModalOpen]);

// w renderze/modalu:
<h2 ref={titleRef} tabIndex="-1">Tytuł modala</h2>

Fokus ustawiasz dopiero, gdy stan i DOM są zaktualizowane, co zapewnia spójne, przewidywalne zachowanie dla screen readerów i użytkowników klawiatury.

ARIA live regions, ogłoszenia i asynchroniczne aktualizacje

Przy aria-live pamiętaj, że zmiana treści trafia do DOM dopiero po zastosowaniu stanu. Nie oczekuj więc, że natychmiast po setState treść będzie nowa – czytnik ekranu ogłosi ją dopiero po renderze. Projektuj komunikaty tak, by użytkownik miał jasny feedback bez blokującej zmiany UI.

setState w komponentach klasowych vs useState – czy różnią się asynchronicznością?

Z perspektywy asynchroniczności fundament jest ten sam: metoda klasowa this.setState(...) i funkcja z hooka useState (const [state, setState] = useState(...)) zapisują intencję zmiany, a React realizuje ją później i przelicza komponent.

Różnice dotyczą głównie API – w klasach masz callback jako drugi argument setState i lifecycle (componentDidUpdate), a w funkcjach reagujesz w useEffect.

„Każde wywołanie setState powoduje ponowne obliczenie komponentu” – tyle że nie od razu, lecz w momencie wybranym przez React.

Najważniejsze zasady na co dzień (checklista)

Na koniec – praktyczna lista rzeczy, które warto mieć w głowie, pisząc komponenty w React:

  1. Traktuj setState / setX jako „plan” zmiany, nie natychmiastową mutację.
    React planowo aktualizuje stan i renderuje komponent później.

  2. Nie opieraj logiki na odczycie stanu zaraz po setState.
    W handlerach i lifecycle nadal odczytasz stary stan.

  3. Używaj funkcjonalnej formy aktualizacji (prev => ...), gdy nowy stan zależy od poprzedniego.
    Zapewnia poprawne wyniki mimo batchowania i wielu wywołań setState w jednym cyklu.

  4. W klasach korzystaj z callbacka setState lub componentDidUpdate.
    Callback jest wywoływany, gdy stan będzie już zaktualizowany.

  5. W komponentach funkcyjnych reaguj na zmiany stanu w useEffect.
    Masz wtedy pewność, że UI został już przerenderowany.

  6. Nie używaj await setState(...) – to nie jest Promise.
    Mimo że aktualizacja jest asynchroniczna, setState nie zwraca obietnicy.

  7. Pamiętaj o dostępności: planuj fokus i komunikaty po aktualizacji stanu.
    Ustawiaj fokus i treści dla aria-live w „fazie po renderze” (np. useEffect), gdy DOM odpowiada nowemu stanowi.

  8. Myśl o wydajności i batchowaniu jako o czymś pozytywnym.
    Asynchroniczne setState i batchowanie istnieją po to, aby aplikacja była płynna, szybka i dostępna, a nie po to, by komplikować kod.