Stav React aplikace: kdy stačí Context a kdy sáhnout po Zustandu
Lokální stav, serverová data, Context a Zustand v Reactu. Jak rozhodnout, kam stav patří, jak se vyhnout zbytečným rerenderům a jak store navrhnout.
„Jakou knihovnu na state management?” je jedna z nejčastějších otázek v Reactu. Správná odpověď začíná jinou otázkou: jaký druh stavu to je? Velká část stavu, který lidé cpou do globálních storů, tam vůbec nepatří.
Čtyři druhy stavu
| Druh | Příklad | Kam s ním |
|---|---|---|
| Lokální UI stav | otevřený dropdown, hodnota inputu | useState v komponentě |
| Serverová data | seznam produktů, detail uživatele | TanStack Query, SWR, nebo framework (Next.js) |
| Stav v URL | filtr, stránka, vyhledávací dotaz | query parametry (?page=2&q=kava) |
| Globální klientský stav | přihlášený uživatel, téma, košík, otevřený panel | Context nebo Zustand |
Serverová data nejsou „globální stav” – je to cache dat ze serveru. Potřebuje řešit načítání, chyby, zastarání, opakování a deduplikaci. Knihovna typu TanStack Query to dělá za vás:
const { data, isPending, error } = useQuery({
queryKey: ['products', category],
queryFn: () => fetch(`/api/products?c=${category}`).then(r => r.json()),
});
Filtry patří do URL, aby šly sdílet odkazem a fungovalo tlačítko Zpět. Když tohle oddělíte, na globální stav často zbude jen pár věcí.
Nejdřív: posunout stav nahoru
Pokud dvě sourozenecké komponenty potřebují stejná data, přesuňte useState do jejich společného rodiče a předejte hodnotu přes props. „Prop drilling” o dvou třech úrovních není problém – je to explicitní a snadno sledovatelné.
React Context: pro málo se měnící hodnoty
Context řeší předání hodnoty hluboko do stromu bez props. Není to state management – je to mechanismus pro předání hodnoty.
// ThemeContext.jsx
import { createContext, useContext, useState, useMemo } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme musí být uvnitř ThemeProvider');
return ctx;
}
function Toggle() {
const { theme, setTheme } = useTheme();
return (
<button onClick={() => setTheme(t => (t === 'light' ? 'dark' : 'light'))}>
Režim: {theme}
</button>
);
}
Vlastní hook useTheme s kontrolou zajistí srozumitelnou chybu, když komponentu použijete mimo provider. useMemo zabrání tomu, aby se při každém renderu provideru vytvořil nový objekt a překreslili se všichni konzumenti.
Hlavní omezení Contextu
Každá komponenta, která volá useContext, se překreslí při jakékoli změně hodnoty – i když používá jen její část. Context nemá selektory. Pro téma, jazyk nebo přihlášeného uživatele (mění se zřídka) je to v pořádku. Pro často se měnící stav (formulář, pozice myši, stav editoru) to znamená zbytečné rerendery velkých částí aplikace.
Částečné řešení je rozdělit Context na víc menších (zvlášť hodnota a zvlášť funkce pro změnu). Když toho začne být hodně, je to signál pro Zustand.
Zustand: malý a rychlý store
npm install zustand
// store/cart.js
import { create } from 'zustand';
export const useCartStore = create((set, get) => ({
items: [],
add: product =>
set(state => {
const existing = state.items.find(i => i.id === product.id);
if (existing) {
return {
items: state.items.map(i =>
i.id === product.id ? { ...i, qty: i.qty + 1 } : i
),
};
}
return { items: [...state.items, { ...product, qty: 1 }] };
}),
remove: id => set(state => ({ items: state.items.filter(i => i.id !== id) })),
clear: () => set({ items: [] }),
total: () => get().items.reduce((s, i) => s + i.price * i.qty, 0),
}));
Žádný provider, store se použije přímo hookem:
function CartBadge() {
const count = useCartStore(state => state.items.length);
return <span className="badge">{count}</span>;
}
function AddButton({ product }) {
const add = useCartStore(state => state.add);
return <button onClick={() => add(product)}>Do košíku</button>;
}
Klíčová výhoda: selektor. CartBadge se překreslí jen když se změní počet položek, AddButton se nepřekreslí nikdy (funkce add je stabilní). U Contextu by se překreslily obě komponenty při každé změně košíku.
set provádí mělké sloučení s existujícím stavem, takže vracíte jen změněné klíče. Stav stejně jako v Reactu neměňte přímo – vracejte nové objekty a pole.
Selektor vracející objekt
// ❌ nový objekt při každém volání → v Zustand v5 nekonečná smyčka / zbytečné rendery
const { items, clear } = useCartStore(s => ({ items: s.items, clear: s.clear }));
// ✅
import { useShallow } from 'zustand/react/shallow';
const { items, clear } = useCartStore(useShallow(s => ({ items: s.items, clear: s.clear })));
Nebo prostě zavolejte hook dvakrát s jednoduchými selektory.
Uložení do localStorage
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export const useCartStore = create(
persist(
(set, get) => ({ /* … stejné jako výše … */ }),
{
name: 'cart',
partialize: state => ({ items: state.items }), // ukládat jen data, ne funkce
}
)
);
Při serverovém renderování (Next.js) pozor: na serveru localStorage neexistuje a první render na klientu má výchozí stav. Komponenty závislé na uloženém stavu může být potřeba vykreslit až po načtení na klientu.
Přístup mimo React
useCartStore.getState().clear(); // např. po odhlášení
const unsub = useCartStore.subscribe(s => console.log(s.items));
Hodí se pro API klienta, WebSocket handlery nebo testy.
Rozhodovací strom
- Používá to jen jedna komponenta? →
useState. - Jsou to data ze serveru? → TanStack Query / SWR / data fetching frameworku.
- Má to jít sdílet odkazem? → URL.
- Potřebují to sourozenci? → posunout stav do rodiče.
- Globální a mění se zřídka (téma, jazyk, uživatel)? → Context.
- Globální, mění se často nebo ho čte hodně komponent? → Zustand.
Redux Toolkit je stále rozumná volba pro velké týmy, které chtějí přísnou strukturu, middleware a výborné DevTools. Pro většinu menších a středních aplikací je Zustand kratší cesta ke stejnému výsledku. Zustand mimochodem umí napojit Redux DevTools přes middleware devtools.