Prop Drilling — problém
Prop drilling nastane, když musíte předávat data skrze více vrstev komponent, přestože mezivrstvy data vůbec nepoužívají. Výsledek: těžko udržovatelný kód.
// ❌ Prop drilling — user se předává skrze 3 vrstvy
function App() {
const [user, setUser] = useState({ name: 'Jana', role: 'admin' });
return <Layout user={user} setUser={setUser} />;
}
function Layout({ user, setUser }) {
return <Sidebar user={user} setUser={setUser} />; // nezajímá ho
}
function Sidebar({ user, setUser }) {
return <UserAvatar user={user} setUser={setUser} />; // nezajímá ho
}
function UserAvatar({ user }) { // konečně potřebuje!
return <img src={user.avatar} alt={user.name} />;
}
Context API
Context umožňuje sdílet data napříč stromem komponent bez prop drillingu. Tři části: createContext, Provider, useContext.
import { createContext, useContext, useState } from 'react';
// 1. Vytvořit Context (s výchozí hodnotou pro TypeScript)
const UserContext = createContext(null);
// 2. Provider — obalí strom komponent
function App() {
const [user, setUser] = useState({ name: 'Jana', role: 'admin' });
return (
<UserContext.Provider value={{ user, setUser }}>
<Layout /> {/* žádné props! */}
</UserContext.Provider>
);
}
// 3. useContext — libovolně hluboko
function UserAvatar() {
const { user } = useContext(UserContext);
return <img src={user.avatar} alt={user.name} />;
}
// TypeScript — typované Context
interface UserContextType {
user: User | null;
setUser: (user: User) => void;
}
const UserContext = createContext<UserContextType | undefined>(undefined);
// Custom hook pro bezpečné použití
function useUser() {
const context = useContext(UserContext);
if (!context) throw new Error('useUser must be used within UserProvider');
return context;
}
// Kvíz: Kdy Context způsobí zbytečné re-rendery?
useReducer — komplexní stav
useReducer je alternativa k useState pro komplexní stavovou logiku. Místo přímého nastavení stavu posíláte akce a čistá funkce reducer rozhoduje, jak se stav změní.
import { useReducer } from 'react';
// Typy akcí
type Action =
| { type: 'INCREMENT' }
| { type: 'DECREMENT' }
| { type: 'SET'; payload: number }
| { type: 'RESET' };
// Reducer — čistá funkce (bez side effects!)
function reducer(state: number, action: Action): number {
switch (action.type) {
case 'INCREMENT': return state + 1;
case 'DECREMENT': return state - 1;
case 'SET': return action.payload;
case 'RESET': return 0;
default: return state; // exhaustive check
}
}
function Counter() {
const [count, dispatch] = useReducer(reducer, 0);
return (
<div>
<p>{count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
<button onClick={() => dispatch({ type: 'SET', payload: 10 })}>= 10</button>
<button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
</div>
);
}
// useReducer vs useState — kdy co?
// useState: jednoduché primitivy, nezávislé hodnoty
// useReducer: objekt s mnoha poli, akce mění více polí,
// logika patří dohromady, testovatelnost
// Kvíz: Musí být reducer v useReducer čistá funkce (pure function)?
Context + Reducer — profesionální pattern
Kombinace Context + useReducer je nejčastěji doporučovaný pattern pro sdílený stav v mid-size React aplikacích bez externích knihoven.
// store/kosik.tsx — kompletní store module
// Typy
interface KosikItem { id: number; nazev: string; cena: number; mnozstvi: number; }
interface KosikState { polozky: KosikItem[]; isOpen: boolean; }
type KosikAction =
| { type: 'PRIDAT'; produkt: Omit<KosikItem, 'mnozstvi'> }
| { type: 'ODEBRAT'; id: number }
| { type: 'TOGGLE_KOSIK' };
// Reducer
function kosikReducer(state: KosikState, action: KosikAction): KosikState {
switch (action.type) {
case 'PRIDAT': {
const exists = state.polozky.find(i => i.id === action.produkt.id);
return {
...state,
polozky: exists
? state.polozky.map(i => i.id === action.produkt.id
? { ...i, mnozstvi: i.mnozstvi + 1 } : i)
: [...state.polozky, { ...action.produkt, mnozstvi: 1 }],
};
}
case 'ODEBRAT':
return { ...state, polozky: state.polozky.filter(i => i.id !== action.id) };
case 'TOGGLE_KOSIK':
return { ...state, isOpen: !state.isOpen };
default: return state;
}
}
// Context
const KosikContext = createContext<{
state: KosikState;
dispatch: Dispatch<KosikAction>;
} | undefined>(undefined);
// Provider
export function KosikProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(
kosikReducer,
{ polozky: [], isOpen: false }
);
return (
<KosikContext.Provider value={{ state, dispatch }}>
{children}
</KosikContext.Provider>
);
}
// Custom hook (s type safety)
export function useKosik() {
const ctx = useContext(KosikContext);
if (!ctx) throw new Error('useKosik mimo KosikProvider');
return ctx;
}
// Použití kdekoliv v app:
const { state, dispatch } = useKosik();
Zustand — minimalistický store
Zustand (německy "stav") je nejpopulárnější lightweight state management library pro React. Žádný boilerplate, žádný Provider, žádné actions/reducers — jen store objekt.
// npm install zustand
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
// Definice store
interface KosikStore {
polozky: KosikItem[];
pridat: (produkt: Product) => void;
odebrat: (id: number) => void;
vymazat: () => void;
celkem: () => number;
}
const useKosik = create<KosikStore>()(
persist( // ← automaticky persist do localStorage!
(set, get) => ({
polozky: [],
pridat: (produkt) => set((state) => {
const existuje = state.polozky.find(i => i.id === produkt.id);
return {
polozky: existuje
? state.polozky.map(i => i.id === produkt.id
? { ...i, mnozstvi: i.mnozstvi + 1 } : i)
: [...state.polozky, { ...produkt, mnozstvi: 1 }],
};
}),
odebrat: (id) => set((state) => ({
polozky: state.polozky.filter(i => i.id !== id),
})),
vymazat: () => set({ polozky: [] }),
celkem: () => get().polozky.reduce(
(sum, i) => sum + i.cena * i.mnozstvi, 0
),
}),
{ name: 'kosik-storage' } // localStorage key
)
);
// Použití — anywhere, no Provider!
function ProductCard({ product }) {
const pridat = useKosik(state => state.pridat); // selector — jen co potřebuji
return <button onClick={() => pridat(product)}>Přidat</button>;
}
function CartBadge() {
const count = useKosik(state => state.polozky.length);
return <span>{count}</span>; // re-renderuje jen při změně count!
}
Kdy co použít
| Situace | Řešení | Proč |
|---|---|---|
| Stav jedné komponenty | useState | Jednoduché, lokální |
| Komplexní logika, více akcí | useReducer | Přehledné akce, testovatelné |
| Sdílená data (téma, auth, jazyk) | Context + useState | Sem-tam čtené, málo změn |
| Sdílená data + komplexní akce | Context + useReducer | Strukturované, bez deps |
| Globální, často měněný stav | Zustand | Selektory, bez Provider, výkon |
| Server state (cache, refetch) | TanStack Query | Caching, invalidace, sync (lekce 8) |
| Velká enterprise app | Redux Toolkit | DevTools, middleware, time-travel |
// Kvíz: Jaká je hlavní výhoda Zustand oproti Context API pro výkonnostně kritický stav?
Cvičení — NotificationContext
Napište NotificationContext — globální toast notifikace. Provider udrží seznam notifikací (text, typ, id), funkce addNotification přidá novou (auto-mizí po 3s), removeNotification odebere.
State Management Expert
Zvládáte Context, useReducer, Zustand patterns a víte kdy co použít.
// Taháček
// Context
const Ctx = createContext(null);
<Ctx.Provider value={...}>{children}</Ctx.Provider>
const value = useContext(Ctx);
// useReducer
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: 'ACTION', payload: data });
// Context + Reducer provider pattern
function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, init);
return <Ctx.Provider value={{ state, dispatch }}>{children}</Ctx.Provider>;
}
// Zustand store
const useStore = create((set, get) => ({
data: [],
add: (item) => set(s => ({ data: [...s.data, item] })),
remove: (id) => set(s => ({ data: s.data.filter(i => i.id !== id) })),
total: () => get().data.length,
}));
// Použití: const data = useStore(s => s.data); // selector!