Lekce 15 · Ekosystém
Správa stavu aplikace
Druhy stavu a jak vybrat: Context vs. Zustand vs. Redux Toolkit.
Načítám lekci…
Lekce 15 · Ekosystém
Druhy stavu a jak vybrat: Context vs. Zustand vs. Redux Toolkit.
Načítám lekci…
Když se stěhuješ, nenaházíš všechno do jedné obří skříně v předsíni. Mléko dáš do lednice, klíče do kapsy, dokumenty do šanonu a společné věci – třeba koření – do spíže, kam sahá celá rodina. Každá věc má místo podle toho, jak se s ní zachází.
Otázka „Redux, nebo Zustand?“ je jako otázka „jakou obří skříň do předsíně koupit?“. Většina stavu v aplikaci do žádné globální skříně nepatří. Za celý kurz už jsme poznali pět míst a každé se hodí na jiný druh stavu:
| Druh stavu | Jako v domácnosti | Příklad | Kam s ním |
|---|---|---|---|
| Serverový | Potraviny v lednici: koupené, s trvanlivostí | Produkty, uživatelé, objednávky | TanStack Query / RTK Query / loadery |
| V adrese | Adresa na obálce | Filtry, stránka, vybraná záložka, hledání | Router (search params) |
| Formulářový | Papíry na pracovním stole | Hodnoty polí, chyby, odesílání | useState / Actions / React Hook Form |
| Lokální UI | Věci v kapse | Otevřený modal, rozbalená sekce | useState / useReducer v komponentě |
| Sdílený klientský | Společná spíž | Přihlášený uživatel, téma, košík, rozpracovaný editor | Context, Zustand, Redux… |
Vezmi stránku e-shopu: seznam produktů s filtrem kategorie a hledáním, tlačítko „Do košíku“, ikonka košíku v hlavičce, rozbalovací detail produktu a přepínač tmavého režimu. Projdeme každý kousek stavu a zeptáme se, kam patří:
| Stav | Otázka | Kam |
|---|---|---|
| Seznam produktů | Patří serveru a může se tam změnit? | TanStack Query |
| Vybraná kategorie, hledaný text | Má přežít poslání odkazu? | Search params |
| Rozbalený detail produktu | Zajímá to někoho jiného než tuhle kartu? | useState v kartě |
| Obsah košíku | Potřebuje ho hlavička i stránka, mění se často? | Zustand / Redux |
| Tmavý režim | Potřebuje ho všechno, mění se zřídka? | Context (nebo i Zustand) |
| Počet položek v košíku | Dá se spočítat z obsahu košíku? | Nikam – spočítej ho (selektor) |
Z šesti věcí jen jedna (košík) opravdu potřebuje sdílený store. To je typické.
Vzpomeň si na lekci o Contextu: Context je rozhlas. Když se změní hlášení, poslouchat musí všichni. Kdyby byl košík v Contextu, každé přidání produktu by překreslilo všechny komponenty, které košík čtou – i tlačítka „Do košíku“, která chtějí jen funkci add.
Store knihoven jako Zustand a Redux funguje spíš jako odběr zpráv na míru. Každá komponenta si řekne, co přesně ji zajímá – „jen počet kusů“, „jen celková cena“, „jen funkce add“ – a dostane zprávu, jen když se změní tohle. Té funkci, která z celého stavu vybere jen to potřebné, se říká selektor:
const count = useCartStore((state) => state.items.length) // „pošli mi jen počet“
Po každé změně store zavolá selektor a porovná výsledek s minulým. Když je stejný, komponenta se nepřekreslí.
import { create } from 'zustand'interface CartStore {items: { product: Product; qty: number }[]add: (product: Product) => voidclear: () => void}const useCartStore = create<CartStore>()((set) => ({items: [], // stavadd: (product) => // akceset((state) => ({ items: [...state.items, { product, qty: 1 }] })),clear: () => set({ items: [] }),}))
create vytvoří store i hook useCartStore najednou. Žádný Provider – store žije mimo React a kdokoli si ho může „předplatit“.set funguje jako setter z useState: dostane předchozí stav a vrátí změny (Zustand je sloučí se zbytkem).Komponenty kdekoli ve stromu si vyberou, co potřebují:
function AddButton({ product }: { product: Product }) {const add = useCartStore((s) => s.add) // funkce se nikdy neměníreturn <button onClick={() => add(product)}>Do košíku</button>}function CartBadge() {const count = useCartStore((s) => s.items.reduce((n, i) => n + i.qty, 0))return <span>🛒 {count}</span>}
Co se stane po kliknutí na „Do košíku“:
add(product) zavolá set a store má nový stav.CartBadge: počet byl 0, teď je 1 → překreslí se.AddButton: funkce add je pořád ta samá → nepřekreslí se.Co vidíš: Tři komponenty, které sdílejí jeden store košíku: tlačítka na přidání, odznak s počtem kusů a celková cena. Každá karta žlutě blikne, když se překreslí.
Vyzkoušej:
add.Co z toho plyne: Každá komponenta dostane zprávu jen o tom, co si vybrala selektorem. Žádný Provider a žádné zbytečné překreslování.
Načítám interaktivní část…
Zustand umí přidat užitečné schopnosti přes tzv. middleware:
import { persist, devtools } from 'zustand/middleware'const useSettings = create<Settings>()(devtools( // napojení na Redux DevToolspersist( // ukládání do localStorage(set) => ({ theme: 'light', setTheme: (theme) => set({ theme }) }),{ name: 'settings' },),),)// Čtení a zápis i mimo React (v utilitách, v testech)useSettings.getState().setTheme('dark')
V lekci o reduceru jsme si vysvětlili bankovní účet: nikdo nepřepisuje zůstatek, všichni jen hlásí transakce a nový stav spočítá jedno místo podle pravidel. Redux je přesně tohle, jen pro celou aplikaci: jeden store, akce, reducery – a k tomu výpis všech transakcí v Redux DevTools.
Redux Toolkit (RTK) je moderní, oficiální způsob psaní Reduxu. Ubírá většinu opakujícího se kódu:
const todosSlice = createSlice({name: 'todos',initialState: { items: [] as Todo[] },reducers: {added(state, action: PayloadAction<string>) {state.items.push({ id: Date.now(), text: action.payload, done: false })},toggled(state, action: PayloadAction<number>) {const todo = state.items.find((t) => t.id === action.payload)if (todo) todo.done = !todo.done},},})export const { added, toggled } = todosSlice.actions // akce se vygenerují samyconst store = configureStore({ reducer: { todos: todosSlice.reducer } })
createSlice = kus stavu + jeho reducery. Z názvů reducerů vytvoří i akce (added('Nakoupit') → { type: 'todos/added', payload: 'Nakoupit' }).configureStore poskládá slices do jednoho store a zapne DevTools.V komponentách se store čte selektory a mění akcemi:
function TodoApp() {const todos = useAppSelector((s) => s.todos.items) // odběr jen téhle částiconst dispatch = useAppDispatch()return <button onClick={() => dispatch(added('Nový úkol'))}>Přidat</button>}// Odvozená data: createSelector je zapamatuje a přepočítá jen při změně vstupůconst selectActive = createSelector([(s: RootState) => s.todos.items], (items) => items.filter((t) => !t.done))
Co vidíš: Seznam úkolů ve store Redux Toolkit: slice s akcemi added, toggled a filterChanged, typované hooky a zapamatovaný selektor pro viditelné úkoly.
Vyzkoušej:
selectVisibleTodos spočítal viditelné úkoly z items a filter.Co z toho plyne: Redux Toolkit je reducer z lekce 5 pro celou aplikaci: akce popisují, co se stalo, slice rozhoduje, jak se změní stav, a selektory vybírají, co kdo potřebuje.
Načítám interaktivní část…
| Context + useReducer | Zustand | Redux Toolkit | |
|---|---|---|---|
| Přirovnání | Rozhlas | Odběr zpráv na míru | Bankovní účet s výpisem |
| Instalace / opakující se kód | Nic / střední | Malá / minimální | Střední / víc struktury |
| Selektory (jemné odběry) | ❌ překreslí všechny posluchače | ✅ | ✅ (+ zapamatované selektory) |
| DevTools, cestování v čase | ❌ | ✅ přes middleware | ✅ vestavěné |
| Serverová data | ❌ | ❌ (kombinuj s TanStack Query) | ✅ RTK Query |
| Kdy | Málo se měnící data: téma, uživatel, jazyk | Střední aplikace, rychlý start, málo pravidel | Velké aplikace a týmy, složitá logika, audit akcí |
Dvě komponenty, jeden společný store. Doplň akci a čtení stavu.
VÝZVA: Počítadlo v Zustandu
Store má `count` a akci increment, která zatím nic nedělá.
Display a Buttons jsou dvě různé komponenty – sdílejí stav přes store, bez props.
1. Ve store doplň increment: set((s) => ({ count: s.count + 1 }))
2. V Display si count přečti selektorem: const count = useCounter((s) => s.count)Načítám interaktivní část…
První výzva procvičí kapitolu 3: oblíbené produkty se nesou přes props přes několik komponent – přesuň je do Zustandu a přidej ukládání. Druhá rozšíří kapitolu 4 o asynchronní načítání v Reduxu.
Z nošení vzkazu přes celou školu na odběr zpráv na míru – a s ukládáním do localStorage.
Střední a těžké výzvy jsou bonus pro přihlášené. Registrace je zdarma – stačí e-mail, žádné heslo.
Kompletní asynchronní tok v Reduxu: pending → fulfilled/rejected (pračka z lekce o reduceru), typované hooky a zapamatovaný selektor.
Střední a těžké výzvy jsou bonus pro přihlášené. Registrace je zdarma – stačí e-mail, žádné heslo.
useShallow, createSelector).create)create – z knihovny zustand: knihovna pro globální stav (lekce 15).row, stack, card, list, btn, input nebo muted jsou hotové styly kurzu v src/styles.css (row = prvky vedle sebe, stack = pod sebou, card = rámeček, muted = šedý text).