Zarządzanie stanem obejmuje wybór miejsca i narzędzia do przechowywania danych wpływających na UI aplikacji. w React to temat, na którym wyłożyło się już setki zespołów. Początkujący dopychają wszystko do Redux. Doświadczeni wrzucają wszystko do useState i Context. Juniorzy kopiują wzorce ze Stack Overflow, które powstały w 2017 roku. Wybór złego narzędzia to kilka tygodni refactoringu później.
Ten artykuł to decision tree oparty na tym, co realnie sprawdza się w projektach, które prowadzę od kilku lat. Bez świętych wojen, bez dogmatów „Redux jest martwy" — konkretne kryteria i rekomendacje.
Pięć rodzajów stanu
Pierwsza rzecz, która porządkuje myślenie — stan w aplikacji Reacta nie jest jednorodny. To pięć różnych rzeczy, które wymagają różnych narzędzi.
1. Lokalny stan UI — otwarte modale, hover state, edit mode toggleów. Żyje w jednym komponencie i nikogo więcej nie interesuje.
2. Stan formularzy — pola, które user wypełnia. Ulotny, żyje do submitu, potem idzie na serwer.
3. Współdzielony stan klienta — dane, które istnieją tylko w przeglądarce, ale używa ich wiele komponentów. Preferencje użytkownika, stan koszyka przed checkoutem, filtry UI.
4. Server state to dane pochodzące z backendu, które mogą być nieaktualne i wymagają cache, synchronizacji oraz refetchingu. — dane pobrane z API. Potencjalnie nieaktualne, wymagają cache'owania, deduplikacji, revalidation.
5. URL state — stan, który powinien być w URL, żeby był udostępnialny i zachowywany w historii przeglądarki.
Każdy z tych pięciu typów stanu ma różne narzędzia, które są dla niego najlepsze. Próba rozwiązania wszystkich jednym hammerem (np. „Redux dla wszystkiego") to główna przyczyna przeinżynierowanych aplikacji.
Decision tree — szybka wersja
Jeśli masz 30 sekund:
| Rodzaj stanu | Narzędzie | Dlaczego |
|---|---|---|
| Lokalny UI | useState, useReducer | Najpierwszy wybór. Zero zależności. |
| Formularze | React Hook Form + Zod | Najwydajniejsze, najbezpieczniejsze |
| Współdzielony client state | Zustand | Lekki, prosty, wystarczający dla 95% |
| Server state | TanStack Query, SWR | Cache + deduplikacja = oczywistość |
| URL state | useSearchParams, nuqs | Linki shareowalne, historia działa |
Redux? Dla bardzo specyficznych przypadków (DevTools z time-travel, masowa współpraca między zespołami, integracja z Redux middleware ecosystem). Dla typowego projektu SaaS w 2026 — nie.
Kiedy useState wystarczy
Pierwsze pytanie, jakie zadaję przy każdym nowym stanie: czy jedynym komponentem, który tego używa, jest ten konkretny komponent?
Jeśli tak — useState i koniec dyskusji. Nie wciskaj Context'u „na przyszłość", nie rób abstrakcji dla jednego use case'u.
Kiedy useState przestaje wystarczać?
- Kilka komponentów potrzebuje tego samego stanu i nie są w relacji parent-child.
- Stan jest złożony (więcej niż 5-7 pól) i zmiany wymagają koordynacji.
- Stan musi przetrwać unmount komponentu (np. zmiana tabu w SPA).
W tym momencie przechodzisz do jednego z poniższych.
Context — kiedy to JEST dobry wybór
Context w React ma reputację „wolnego", ale większość problemów wydajnościowych z Context bierze się z jego nadużywania. Używany zgodnie z jego przeznaczeniem, jest świetny.
Context jest dobry dla:
- Stanu, który się rzadko zmienia. Motyw (dark/light), język, user sesja, feature flags. Raz ustawione, stoi.
- Dependency injection. Przekazanie instancji API client, configu, loggera, db connection w dół drzewa komponentów.
- Małych domen funkcjonalnych.
WizardProviderdla multi-step formularza,ModalProviderdla zarządzania modalami.
Context jest ZŁY dla:
- Stanu, który zmienia się często (counter, live values).
- Stanu, który ma dużo komponentów-konsumentów i każdy chce tylko fragmentu.
- Aplikacji, gdzie performance ma znaczenie (Context propaguje wszystkim konsumentom przy każdej zmianie — bez selectorów).
To jest Context w jego naturalnym habitacie.
Zustand — mój default dla client state
Zustand (z niemieckiego „stan") to mała (~3 KB gzipped) biblioteka, która robi jedną rzecz dobrze: pozwala zdefiniować store z selectorami, bez boilerplatu, bez providera.
Co kocham w Zustand:
- Brak providera. Nie musisz owijać aplikacji w
<Provider store={store}>. - Selectory są granularne. Komponent re-renderuje się tylko, gdy zmieni się konkretny fragment, który selector zwraca.
- Zero boilerplate'u. Nie ma actions, reducers, dispatch, slices. Masz store, masz funkcje, koniec.
- TypeScript działa out of the box. Autocomplete, type inference, bez generics explosion.
- Middleware.
persistdo localStorage,devtoolsdo Redux DevTools,subscribeWithSelectordla reaktywnych side effects.
Kiedy Zustand jest strzałem w dziesiątkę:
- Stan współdzielony przez wiele komponentów w SPA.
- Aplikacja średniej wielkości (SaaS, dashboard, editor).
- Zespół, który chce pragmatyzmu bez boilerplate'u.
Typowe use case'y:
- Koszyk zakupów przed checkoutem.
- Stan UI aplikacji (sidebar otwarty/zamknięty, zoom level, current tab).
- Statyczna konfiguracja klienta, która po starcie aplikacji zachowuje się jak client state.
- Preferencje usera, które persistują w localStorage.
W Army Builder używam Zustand do zarządzania konfiguracją armii i filtrów — stan, który user manipuluje przez dziesiątki komponentów, persistuje w localStorage, i nie ma sensu trzymać w URL.
Zustand w Next.js App Router
W zwykłym SPA globalny store Zustand jest prosty. W Next.js App Router trzeba uważać, bo serwer może obsługiwać wiele requestów równolegle, a modułowy singleton jest współdzielony między requestami.
Bezpieczny wzorzec:
- Twórz store per request albo per provider, nie jako globalny mutable singleton używany przez RSC.
- Nie czytaj i nie zapisuj Zustand store w React Server Components.
- Jeśli store ma dane początkowe z serwera, przekaż je jako props do client provider i zainicjalizuj store raz.
- Uważaj na hydration mismatch przy
persist; część danych zlocalStorageistnieje dopiero po stronie klienta. - Server state nadal trzymaj w TanStack Query/SWR, a nie w Zustand.
Redux Toolkit to oficjalny, uproszczony zestaw narzędzi do pracy z Reduxem: createSlice, configureStore i standardowe middleware. — kiedy wciąż warto
Redux w 2026 jest znacznie mniejszą częścią ekosystemu niż był 5 lat temu, ale nie zniknął. Redux Toolkit (RTK) to nowoczesny, skondensowany Redux bez dawnego boilerplate'u.
Redux Toolkit wybierasz, gdy:
- Zespół jest duży (20+ developerów). Redux narzuca bardziej sztywne konwencje, co pomaga w kodzie pisanym przez wielu ludzi.
- Masz złożone side effects wymagające sag.
redux-sagaz generatorami daje kontrolę, której nie osiągniesz w Zustand. Przykład: długotrwałe transakcje, retry logic, cancel-on-navigate patterns. - Potrzebujesz time-travel debugging. Redux DevTools z replay action history są unikalne.
- Integrujesz z legacy kodem Redux. Projekt już używa Reduxa, migracja nie jest priorytetem.
RTK Query (jego built-in data fetching) to rozsądna alternatywa dla TanStack Query, jeśli już masz RTK. Integruje się natywnie ze store.
Porównaj z wersją Zustand powyżej. RTK wymaga więcej struktury, ale daje konsystencję, która w dużym zespole ma wartość.
Dla nowego projektu, który mogę rozpocząć od zera — nie wybieram Redux. Dla projektu, który już używa Redux, migracja na Zustand nie jest wysokim priorytetem (a często nie ma sensu w ogóle).
TanStack Query / SWR — server state
To jest osobna kategoria i najczęstszy błąd: ludzie trzymają server data w Reduxie lub Zustand. Tracą wtedy wszystko, co TanStack Query daje za darmo:
- Cache per-queryKey.
- Deduplikacja (kilka komponentów używających tego samego query robią jeden request).
- Background refetch (stale-while-revalidate).
- Retry z exponential backoff.
- Optimistic updates z rollbackiem.
- Infinite queries, prefetching, pagination.
Pięć linii, dostajesz wszystko wymienione wyżej. Szczegółowo porównuję TanStack Query z SWR i useEffect w artykule o fetchingu danych.
Zasada: jeśli dane pochodzą z serwera, trzymaj je w Query, nie w Zustand/Redux. Store jest dla czystego client state.
URL state — najbardziej niedoceniane
URL to najłatwiejszy store, jaki masz. Współdzielony z innymi tab'ami, zapisywany w historii, shareowalny, persistowany w bookmarku. Dla typu stanu, który powinien być w URL, każde inne rozwiązanie jest pomyłką.
Typowe rzeczy, które MUSZĄ być w URL:
- Filtry w listach (
?category=astro&tag=performance). - Sortowanie (
?sort=price&order=asc). - Paginacja (
?page=3). - Search query (
?q=react%20hooks). - Tab index w aplikacji z tabami (
/settings/profile).
Użycie native React (App Router):
Dla projektów z dużą ilością URL state polecam nuqs — biblioteka, która daje typowany hook useQueryState podobny do useState, ale z URL jako backing store:
Typowany, zwięzły, integracja z Next.js App Router out of the box.
Typowe błędy
W audytach kodu widuję regularnie te trzy:
1. Server state w Zustand/Redux. „Pobraliśmy userów, wrzućmy do store". Następnie zespół pisze własną cache logic, dedup, staleness check. Totalne marnotrawstwo — TanStack Query jest do tego zaprojektowany.
2. URL state w Zustand. „Filtruj po kategorii" ustawia wartość w store. User kopiuje URL i wysyła znajomemu — znajomy widzi inny stan, bo store jest pusty. Użyj URL.
3. Context dla wszystkiego. „Szybko wrzućmy to do Context, żeby nie przekazywać propsów przez 5 poziomów". Na początku działa, potem okazuje się, że wszystko re-renderuje się przy każdej zmianie. Gdy to wykryjesz, jesteś głęboko w bagnie. Zustand to rozwiązuje od pierwszego dnia.
Decision tree — pełna wersja
Mój flow decyzji dla każdego nowego stanu:
- Czy używa tego tylko jeden komponent? →
useState/useReducer. - Czy powinno być w URL (filtry, sort, pagination, tab)? → URL state (
useSearchParams,nuqs). - Czy to server data (z API)? → TanStack Query / SWR.
- Czy to formularz? → React Hook Form + Zod (lub React 19 Actions dla prostych przypadków).
- Czy zmienia się rzadko i to de facto config/dep injection? → Context.
- Wszystko inne (współdzielony client state) → Zustand.
Redux? Rozważam tylko, gdy jestem w dużym zespole z istniejącym ecosystemem Redux lub mam specyficzne wymagania (time-travel debugging, sagas).
Podsumowanie
„Co wybrać do zarządzania stanem" to pytanie, na które nie ma jednej odpowiedzi, bo stan nie jest jednorodny. Pięć typów stanu — pięć różnych narzędzi. Zustand jako default dla współdzielonego client state, TanStack Query dla server data, URL dla filtrów, Context dla dep injection, useState dla lokalnego.
Dla typowego projektu w 2026 Redux to overkill. Ale każdy projekt to osobna rozmowa. Jeśli zaczynasz projekt w React i chcesz pomóc z wyborem stack'u, odezwij się — 30-minutowa rozmowa zaoszczędza tygodnie refactoringu.
