StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

Doradztwo produktowe

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

Doradztwo produktowe

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

Doradztwo produktowe

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

RealizacjeO mnieBlog
Porozmawiajmy
PL
EN

Nowoczesne strony internetowe dla firm, które myślą odważnie.

Przewiń do góry

Nazwa

StriveLab Maciej Sala

NIP

6772218995

REGON

524008527

E-mail

contact@strivelab.pl

Usługi główne
  • Tworzenie stron internetowych
  • Strony internetowe Next.js
  • Strony internetowe Astro
  • Strony internetowe React
Inne usługi
  • Usługi
  • SEO & Performance Sprint
  • QA & Stabilizacja
  • Konsultacje Product / Delivery
  • Aplikacje webowe Next.js
  • Współpraca ciągła
Strony
  • O mnie
  • Usługi
  • Realizacje
  • Blog

© 2026 StriveLab.pl

Polityka prywatności
ReactState ManagementArchitecture

Zustand vs Redux Toolkit vs Context vs URL state — decision tree dla realnych projektów

Jak wybrać narzędzie do zarządzania stanem w React? Decision tree dla Zustand, Redux Toolkit, Context i URL state. Kiedy co realnie sprawdza się w produkcyjnych aplikacjach.

OpublikujLinkedInFacebookWyślij
Autor
Maciej Sala
Opublikowano
13 maja 2026 08:17
Czytanie
8 min czytania
Aktualizacja
Wersja pierwotna

W skrócie

  • Najpierw rozróżnij lokalny UI state, formularze, client state, server state i URL state to stan zapisany w adresie strony, np. filtry, sortowanie, paginacja lub aktywna zakładka..
  • Nie wkładaj server state do Zustand lub Redux, bo TanStack Query i SWR rozwiązują ten problem lepiej.
  • URL state powinien mieszkać w adresie, jeśli wpływa na filtry, sortowanie, paginację lub aktywny tab.
  • Zustand jest dobrym domyślnym wyborem dla współdzielonego client state, ale nie dla wszystkiego.

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 stanuNarzędzieDlaczego
Lokalny UIuseState, useReducerNajpierwszy wybór. Zero zależności.
FormularzeReact Hook Form + ZodNajwydajniejsze, najbezpieczniejsze
Współdzielony client stateZustandLekki, prosty, wystarczający dla 95%
Server stateTanStack Query, SWRCache + deduplikacja = oczywistość
URL stateuseSearchParams, nuqsLinki 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?

Code
// Użycie useState — lokalny stan, nic więcej
function SearchBox() {
  const [query, setQuery] = useState('');
 
  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

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. WizardProvider dla multi-step formularza, ModalProvider dla 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).
Code
// Context jako dependency injection — idealne użycie
const ApiContext = createContext<ApiClient>(null!);
 
function ApiProvider({ children }: { children: React.ReactNode }) {
  const api = useMemo(() => new ApiClient(process.env.API_URL!), []);
  return <ApiContext.Provider value={api}>{children}</ApiContext.Provider>;
}
 
function useApi() {
  return useContext(ApiContext);
}
 
// Konsument
function UserProfile({ userId }) {
  const api = useApi();
  const { data } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => api.getUser(userId),
  });
  // ...
}

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.

Code
import { create } from 'zustand';
 
type CartStore = {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
  total: () => number;
};
 
export const useCart = create<CartStore>((set, get) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
  removeItem: (id) => set((state) => ({
    items: state.items.filter((i) => i.id !== id),
  })),
  total: () => get().items.reduce((sum, item) => sum + item.price, 0),
}));
 
// Użycie
function CartSummary() {
  const total = useCart((state) => state.total());  // selector
  return <p>Total: {total} zł</p>;
}
 
function AddToCartButton({ product }) {
  const addItem = useCart((state) => state.addItem);
  return <button onClick={() => addItem(product)}>Add to cart</button>;
}

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. persist do localStorage, devtools do Redux DevTools, subscribeWithSelector dla 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 z localStorage istnieje dopiero po stronie klienta.
  • Server state nadal trzymaj w TanStack Query/SWR, a nie w Zustand.
Uwaga

Największy błąd w Next.js to store utworzony w module i używany jak globalna pamięć aplikacji. W SPA to działa, na serwerze może oznaczać przeciek stanu między requestami.

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-saga z 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.

Code
import { createSlice, configureStore } from '@reduxjs/toolkit';
 
const cartSlice = createSlice({
  name: 'cart',
  initialState: { items: [] },
  reducers: {
    addItem: (state, action) => {
      state.items.push(action.payload);  // Immer — „mutacja" OK
    },
    removeItem: (state, action) => {
      state.items = state.items.filter((i) => i.id !== action.payload);
    },
  },
});
 
export const { addItem, removeItem } = cartSlice.actions;
export const store = configureStore({ reducer: { cart: cartSlice.reducer } });

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.
Code
import { useQuery } from '@tanstack/react-query';
 
function ProductList({ category }: { category: string }) {
  const { data: products, isLoading } = useQuery({
    queryKey: ['products', category],
    queryFn: () => fetchProducts(category),
  });
 
  if (isLoading) return <Spinner />;
  return <List items={products} />;
}

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):

Code
'use client';
 
import { useSearchParams, useRouter } from 'next/navigation';
 
function Filters() {
  const searchParams = useSearchParams();
  const router = useRouter();
 
  const category = searchParams.get('category') ?? 'all';
 
  const setCategory = (value: string) => {
    const params = new URLSearchParams(searchParams);
    params.set('category', value);
    router.push(`?${params.toString()}`);
  };
 
  return (
    <select value={category} onChange={e => setCategory(e.target.value)}>
      <option value="all">Wszystkie</option>
      <option value="astro">Astro</option>
      <option value="react">React</option>
    </select>
  );
}

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:

Code
import { useQueryState } from 'nuqs';
 
function Filters() {
  const [category, setCategory] = useQueryState('category', { defaultValue: 'all' });
  const [sort, setSort] = useQueryState('sort', { defaultValue: 'date' });
  const [page, setPage] = useQueryState('page', { defaultValue: 1, parse: parseInt });
 
  // ...
}

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:

  1. Czy używa tego tylko jeden komponent? → useState / useReducer.
  2. Czy powinno być w URL (filtry, sort, pagination, tab)? → URL state (useSearchParams, nuqs).
  3. Czy to server data (z API)? → TanStack Query / SWR.
  4. Czy to formularz? → React Hook Form + Zod (lub React 19 Actions dla prostych przypadków).
  5. Czy zmienia się rzadko i to de facto config/dep injection? → Context.
  6. 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.

Często zadawane pytania

Pracuję z tym zawodowo.

Jeśli chcesz uporządkować frontend, architekturę React i Next.js, poprawić jakość wdrożenia albo przyspieszyć development bez psucia maintainability, skontaktuj się ze mną. Na co dzień pracuję hands-on przy projektach, w których kod, UX i decyzje produktowe muszą działać razem.

Skontaktuj się ze mną
Maciej Sala

O autorze

Maciej Sala

Maciej Sala — project manager i frontendowiec z doświadczeniem w marketingu internetowym. Na co dzień pracuję z Reactem, Next.js i TypeScriptem, łącząc perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat związany z branżą gier wideo jako project manager i game designer.

Absolwent historii na Uniwersytecie Jagiellońskim i studiów podyplomowych z marketingu internetowego na Akademii Górniczo-Hutniczej w Krakowie. Poza pracą trenuje na siłowni, maluje figurki i realizuje własne projekty.

Moje artykułyWięcej o mnie

Seria

React w praktyce 2026
  1. 1React 19 Actions — formularz bez onSubmit, useOptimistic i useActionState w praktyce
  2. 2React Compiler w 2026 — czy useMemo i useCallback są już martwe?
  3. 3React Query (TanStack) vs SWR vs useEffect — kompletny przewodnik po fetchingu w 2026
  • Pięć rodzajów stanu1 min
  • Decision tree — szybka wersja1 min
  • Kiedy useState wystarczy1 min
  • Context — kiedy to JEST dobry wybór1 min
  • Zustand — mój default dla client state1 min
  • Zustand w Next.js App Router1 min
  • <Term tip="Redux Toolkit to oficjalny, uproszczony zestaw narzędzi do pracy z Reduxem: createSlice, configureStore i standardowe middleware.">Redux Toolkit</Term> — kiedy wciąż warto1 min
  • TanStack Query / SWR — server state1 min
  • URL state — najbardziej niedoceniane1 min
  • Typowe błędy1 min
  • Decision tree — pełna wersja1 min
  • Podsumowanie1 min

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów
SEO w Astro — Core Web Vitals, dane uporządkowane i techniczny fundament rankingu w 2026
SEO w Astro — Core Web Vitals, dane uporządkowane i techniczny fundament rankingu w 2026

Jak zbudować stronę w Astro, która dominuje w SEO — Core Web Vitals, sitemap, robots.txt, metadane, dane uporządkowane i GEO/AEO. Przewodnik techniczny z konkretnymi implementacjami.

Maciej Sala

Maciej Sala

Founder Strivelab

13 maja 2026
React Query (TanStack) vs SWR vs useEffect — kompletny przewodnik po fetchingu w 2026
React Query (TanStack) vs SWR vs useEffect — kompletny przewodnik po fetchingu w 2026

Jak pobierać dane w React w 2026? Porównanie TanStack Query, SWR i useEffect. Kiedy Server Components wystarczą, kiedy potrzebujesz cache, invalidation, optimistic updates i infinite queries.

Maciej Sala

Maciej Sala

Founder Strivelab

13 maja 2026
React Compiler w 2026 — czy useMemo i useCallback są już martwe?
React Compiler w 2026 — czy useMemo i useCallback są już martwe?

React Compiler stabilny od 2025. Pokazuję, kiedy ręczna memoizacja traci sens, kiedy nadal ma znaczenie i jak realnie wdrożyć Compiler w istniejącym projekcie React.

Maciej Sala

Maciej Sala

Founder Strivelab

13 maja 2026