Lekce 17 · Shrnutí
Shrnutí: principy Reactu
15 principů celého kurzu s vysvětlením a příklady „takhle ne / takhle ano“.
Načítám lekci…
Lekce 17 · Shrnutí
15 principů celého kurzu s vysvětlením a příklady „takhle ne / takhle ano“.
Načítám lekci…
Celým kurzem tě provázely obrazy z obyčejného života: kuchař bez paměti, sešit, bankovní účet, školní rozhlas, hotel s recepcí. Měly jediný účel – aby sis u každého tématu vybavil, proč funguje tak, jak funguje. Až budeš psát vlastní aplikaci, nebudeš mít po ruce lekci, ale tyhle obrazy si vybavíš.
Tahle lekce nepřináší nic nového. Bere všechno, co jsi v kurzu viděl, a skládá to do patnácti principů, ze kterých se dá odvodit skoro každé rozhodnutí při psaní Reactu. Ke každému principu najdeš přirovnání, vysvětlení, proč platí, příklad „takhle ne / takhle ano“ a odkaz na lekci, kde je téma rozebrané podrobně.
Celý React stojí na jedné rovnici: UI = f(stav). Komponenta je funkce, která z props a stavu spočítá, jak má obrazovka vypadat. Nic nekreslí ani neupravuje sama – vrátí popis (JSX) a React se postará, aby DOM odpovídal.
Z toho plynou tři pravidla:
// ❌ Nečistý render: mění data mimo sebe a výsledek závisí na tom, kolikrát se zavolallet renderCount = 0function Guest({ name }: { name: string }) {renderCount++return <li>#{renderCount} {name}</li>}// ✅ Čistý render: všechno potřebné přichází zvenkufunction Guest({ name, order }: { name: string; order: number }) {return <li>#{order} {name}</li>}
// ❌ Imperativně: „po kliknutí schovej tlačítko, ukaž spinner, pak ukaž zprávu…“// ✅ Deklarativně: popiš každý stav zvlášť, React vyřeší přechodyfunction Submit({ status }: { status: 'idle' | 'sending' | 'sent' }) {if (status === 'sending') return <Spinner />if (status === 'sent') return <p>Odesláno ✔</p>return <button type="submit">Odeslat</button>}
📚 Podrobně v lekci Rychlé opakování základů.
Do stavu patří jen to, co se mění v čase a nedá se spočítat z ničeho jiného. Všechno ostatní je prop, konstanta, nebo hodnota spočítaná při renderu. Každá informace má mít právě jedno místo, kde žije – jakmile ji máš dvakrát, dřív nebo později se ty kopie rozejdou.
// ❌ Duplicitní stav + efekt, který ho „dohání“const [todos, setTodos] = useState<Todo[]>([])const [filter, setFilter] = useState<'all' | 'done'>('all')const [visible, setVisible] = useState<Todo[]>([])const [doneCount, setDoneCount] = useState(0)useEffect(() => {setVisible(filter === 'all' ? todos : todos.filter((t) => t.done))setDoneCount(todos.filter((t) => t.done).length)}, [todos, filter])// → render navíc, na okamžik zastaralé hodnoty, víc míst pro chybu// ✅ Stav jen to nutné, zbytek spočítatconst [todos, setTodos] = useState<Todo[]>([])const [filter, setFilter] = useState<'all' | 'done'>('all')const visible = filter === 'all' ? todos : todos.filter((t) => t.done)const doneCount = todos.filter((t) => t.done).length
Stejný princip platí i pro výběr: neukládej si kopii vybraného objektu, ukládej jeho ID.
// ❌ Kopie: po přejmenování položky ukazuje detail staré jménoconst [selected, setSelected] = useState<Item | null>(null)// ✅ ID + dohledání při renderu: vždy aktuální dataconst [selectedId, setSelectedId] = useState<string | null>(null)const selected = items.find((i) => i.id === selectedId) ?? null
| Otázka | Když ano… |
|---|---|
| Přichází to od rodiče? | Je to prop, ne stav. |
| Nikdy se to nemění? | Je to konstanta (klidně mimo komponentu). |
| Dá se to spočítat z jiného stavu/props? | Spočítej to při renderu (případně useMemo, když je to drahé). |
| Je to kopie něčeho, co už ve stavu je? | Ulož jen ID nebo klíč. |
| Mění se to a UI na to musí reagovat? | Teprve teď je to stav. |
📚 Podrobně v lekci Stav a události.
Hodnota stavu je v rámci jednoho renderu zamrzlá. setCount(count + 1) nemění count hned – naplánuje nový render, ve kterém bude count nový. Všechny handlery a efekty z daného renderu vidí pořád jeho snímek.
function Counter() {const [count, setCount] = useState(0)const addThree = () => {// ❌ Všechny tři řádky čtou stejný snímek (0) → výsledek 1setCount(count + 1)setCount(count + 1)setCount(count + 1)}const addThreeOk = () => {// ✅ Updater dostane vždy nejnovější hodnotu → výsledek 3setCount((c) => c + 1)setCount((c) => c + 1)setCount((c) => c + 1)}}
React pozná změnu porovnáním reference (Object.is). Když objekt nebo pole zmutuješ, reference zůstane stejná a React nic nepřekreslí. Proto se stav nikdy nemění, jen nahrazuje novou hodnotou.
// ❌ Mutace – stejná reference, React změnu nevidítodos.push(newTodo); setTodos(todos)user.name = 'Eva'; setUser(user)// ✅ Nové hodnotysetTodos((prev) => [...prev, newTodo]) // přidánísetTodos((prev) => prev.filter((t) => t.id !== id)) // odebránísetTodos((prev) => prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))) // úpravasetUser((prev) => ({ ...prev, name: 'Eva' })) // objektsetItems((prev) => prev.toSorted(byName)) // řazení bez mutace
📚 Podrobně v lekci Stav a události.
Nejčastější architektonická chyba v Reactu je dát stav na špatné místo – typicky všechno do jednoho globálního store, nebo serverová data do useState. Pomáhá rozlišit, o jaký druh stavu jde, a podle toho zvolit nástroj. Obecně platí: drž stav co nejníž to jde a zvedej ho až tehdy, když ho potřebuje víc komponent.
| Druh stavu | Příklad | Kam patří |
|---|---|---|
| Lokální UI stav | otevřené menu, text v inputu, hover | useState |
| Sdílený mezi sourozenci | vybraná položka v seznamu + detail | zvednout do nejbližšího společného rodiče |
| Složitý s mnoha přechody | průvodce o 4 krocích, editor | useReducer |
| Málo se měnící, potřebný hluboko | téma, přihlášený uživatel, jazyk | Context |
| Má přežít reload / jde sdílet odkazem | filtry, stránka, řazení, otevřený tab | URL (search params, route params) |
| Data ze serveru | seznam produktů, profil | TanStack Query (cache) |
| Klientský stav celé aplikace, často měněný | košík, rozpracovaný dokument | Zustand / Redux Toolkit |
// Lifting state up: dva sourozenci potřebují stejný údaj → přesune se do rodičefunction Inbox() {const [selectedId, setSelectedId] = useState<string | null>(null)return (<><MessageList selectedId={selectedId} onSelect={setSelectedId} /><MessageDetail id={selectedId} /></>)}
📚 Podrobně v lekci Správa stavu aplikace.
Kód v komponentě má tři možná místa: render (výpočet UI), handler (reakce na konkrétní akci uživatele) a efekt (synchronizace s něčím mimo React). Ptej se: Proč se tenhle kód má spustit?
// ❌ Efekt jako „reakce na událost“ – nepřímé, křehké, spustí se i při jiné změněconst [submitted, setSubmitted] = useState(false)useEffect(() => {if (submitted) {post('/api/order', cart)showToast('Objednáno!')}}, [submitted])// ✅ Logika patří tam, kde událost vzniklaasync function handleOrder() {await post('/api/order', cart)showToast('Objednáno!')}
Když už efekt píšeš, musí umět začít i skončit: co efekt otevře, to cleanup zavře. Strict Mode efekty schválně spouští dvakrát (připojit → odpojit → připojit), aby chybějící cleanup odhalil.
// ✅ Synchronizace s externím systémem + úklid + ošetření race conditionuseEffect(() => {const controller = new AbortController()fetch(`/api/users/${userId}`, { signal: controller.signal }).then((r) => r.json()).then(setUser).catch((e) => { if (e.name !== 'AbortError') setError(e) })// Změní se userId dřív, než přijde odpověď? Starý požadavek zrušíme,// takže pomalá stará odpověď nepřepíše novější data.return () => controller.abort()}, [userId]) // všechny reaktivní hodnoty, které efekt čte
📚 Podrobně v lekci Efekty (useEffect).
useRef si pamatuje hodnotu mezi rendery, ale její změna nevyvolá render. Proto se hodí na věci, které uživatel nevidí: DOM element, ID časovače, předchozí hodnotu, instanci knihovny. Co se má ukázat v UI, patří do stavu.
function Stopwatch() {const [elapsed, setElapsed] = useState(0) // ✅ vidí ho uživatel → stavconst intervalRef = useRef<number | null>(null) // ✅ jen technický údaj → refconst start = () => {intervalRef.current = window.setInterval(() => setElapsed((e) => e + 1), 1000)}const stop = () => {if (intervalRef.current !== null) clearInterval(intervalRef.current)}// …}// ❌ Ref místo stavu: hodnota se změní, ale obrazovka zůstane staráconst countRef = useRef(0)<button onClick={() => countRef.current++}>{countRef.current}</button>
S DOM pracuj přes ref jen tam, kde to deklarativně nejde: fokus, scroll, měření, přehrávání videa. V Reactu 19 je ref obyčejný prop, takže forwardRef už nepotřebuješ.
// React 19: ref jako běžný propfunction TextInput({ ref, ...props }: React.ComponentProps<'input'>) {return <input ref={ref} {...props} />}const inputRef = useRef<HTMLInputElement>(null)<TextInput ref={inputRef} /><button onClick={() => inputRef.current?.focus()}>Upravit</button>
📚 Podrobně v lekci Refy (useRef).
Když stav mění víc handlerů různými způsoby, logika se rozprostře po celé komponentě. Reducer ji soustředí na jedno místo: komponenty jen hlásí události (card_moved), reducer rozhoduje, jak z nich vznikne nový stav. Je to čistá funkce, takže ji jde testovat bez UI a snadno do ní přidat třeba undo.
type Action =| { type: 'item_added'; name: string }| { type: 'item_toggled'; id: string }| { type: 'cleared_done' }function reducer(state: Item[], action: Action): Item[] {switch (action.type) {case 'item_added':return [...state, { id: crypto.randomUUID(), name: action.name, done: false }]case 'item_toggled':return state.map((i) => (i.id === action.id ? { ...i, done: !i.done } : i))case 'cleared_done':return state.filter((i) => !i.done)}}const [items, dispatch] = useReducer(reducer, [])dispatch({ type: 'item_toggled', id }) // ✅ „co se stalo“// místo setItems(items.map(...)) roztroušeného v pěti handlerech
| useState | useReducer |
|---|---|
| Jednoduché, nezávislé hodnoty | Několik hodnot, které se mění spolu |
| Pár způsobů, jak se stav mění | Hodně různých přechodů |
| Logika je triviální | Logiku chceš testovat nebo sdílet (např. s Contextem) |
📚 Podrobně v lekci Složitější stav (useReducer).
Context sám žádný stav nedrží – jen doručí hodnotu hluboko do stromu bez prop drillingu. Stav dál žije v useState/useReducer v Provideru. Každá změna hodnoty překreslí všechny konzumenty, takže se hodí na data, která se mění zřídka.
// ✅ Vzor: Provider + vlastní hook s kontrolouconst ThemeContext = createContext<Theme | null>(null)export function ThemeProvider({ children }: { children: React.ReactNode }) {const [theme, setTheme] = useState<'light' | 'dark'>('light')const value = useMemo(() => ({ theme, setTheme }), [theme]) // stabilní referencereturn <ThemeContext value={value}>{children}</ThemeContext>}export function useTheme() {const ctx = use(ThemeContext)if (!ctx) throw new Error('useTheme musí být uvnitř <ThemeProvider>')return ctx}
📚 Podrobně v lekci Context.
Vlastní hook je obyčejná funkce začínající na use, která volá jiné hooky. Každá komponenta, která ho zavolá, dostane vlastní nezávislou kopii stavu. Hook neslouží ke sdílení dat (to je Context nebo store), ale ke znovupoužití chování a k pojmenování záměru.
function useDebouncedValue<T>(value: T, delay = 300) {const [debounced, setDebounced] = useState(value)useEffect(() => {const id = setTimeout(() => setDebounced(value), delay)return () => clearTimeout(id)}, [value, delay])return debounced}// Komponenta čte jako věta – co dělá, ne jakfunction Search() {const [query, setQuery] = useState('')const debouncedQuery = useDebouncedValue(query)const { data } = useQuery({ queryKey: ['search', debouncedQuery], queryFn: () => search(debouncedQuery) })// …}
📚 Podrobně v lekci Vlastní hooky.
Ne každé pole musí být controlled. Když hodnotu potřebuješ až při odeslání, nech ji v DOM a přečti ji z FormData. Controlled input (value + onChange) má smysl, když na hodnotu musíš reagovat při psaní – živá validace, formátování, závislá pole.
React 19 Actions k tomu přidávají pending stav, chyby a optimistické UI bez ručních isLoading proměnných.
async function subscribe(prev: State, formData: FormData): Promise<State> {const email = String(formData.get('email'))if (!email.includes('@')) return { error: 'Neplatný e-mail' }await api.subscribe(email)return { error: null, done: true }}function Newsletter() {const [state, formAction, isPending] = useActionState(subscribe, { error: null })return (<form action={formAction}><input name="email" defaultValue="" /> {/* uncontrolled – stačí */}<button disabled={isPending}>{isPending ? 'Odesílám…' : 'Odebírat'}</button>{state.error && <p role="alert">{state.error}</p>}</form>)}
📚 Podrobně v lekci Formuláře a React 19 Actions.
Render není drahý – drahé jsou zbytečné změny DOM a těžké výpočty. React DOM mění jen tam, kde se výsledek liší. Než začneš memoizovat, změř problém v React DevTools Profileru a zkus ho vyřešit strukturou:
memo, useMemo, useCallback (nebo React Compiler, který to udělá za tebe).// ❌ Psaní do inputu překresluje i drahý graffunction Dashboard() {const [query, setQuery] = useState('')return (<><input value={query} onChange={(e) => setQuery(e.target.value)} /><ExpensiveChart /></>)}// ✅ Stav posunutý dolů – ExpensiveChart se nepřekresluje vůbec, bez jediného memofunction Dashboard() {return (<><SearchBox /> {/* query žije uvnitř */}<ExpensiveChart /></>)}
| Nástroj | Co dělá | Kdy |
|---|---|---|
memo(Comp) | přeskočí render při stejných props | drahá komponenta, často se stejnými props |
useMemo | zapamatuje výsledek výpočtu | drahý výpočet, nebo stabilní objekt pro memo/závislost |
useCallback | zapamatuje funkci | funkce jde do memo komponenty nebo do závislostí |
useTransition | označí aktualizaci jako neurgentní | přepnutí záložky, filtrování velkého seznamu |
useDeferredValue | zpožděná kopie hodnoty | hodnotu nevlastníš (prop), chceš plynulý input |
📚 Podrobně v lekci Výkon a renderování.
Komponenta, která se řídí desítkou props (showHeader, headerIcon, footerButtons…), roste s každým novým požadavkem. Komponenta, která přijímá obsah přes children a sloty, se nemusí měnit vůbec.
// ❌ Konfigurace: každá nová varianta = nový prop<Card title="Objednávka" showIcon icon="cart" footerText="Zaplatit" onFooterClick={pay} />// ✅ Kompozice: Card řeší jen rámeček, obsah dodá ten, kdo ji používá<Card><Card.Header><CartIcon /> Objednávka</Card.Header><OrderItems /><Card.Footer><button onClick={pay}>Zaplatit</button></Card.Footer></Card>
Další vzory ze stejné rodiny:
Tabs, Tabs.List, Tabs.Panel) – sdílí stav přes Context, uživatel skládá strukturu.overflow a z-index), ale v React stromu zůstane, takže context i události fungují normálně.📚 Podrobně v lekci Návrhové vzory komponent.
TypeScript nejvíc pomůže, když typ popisuje jen platné stavy. Několik nezávislých booleanů a nepovinných polí dovolí kombinace, které nedávají smysl (isLoading a zároveň error). Discriminated union je zakáže už při psaní.
// ❌ 2 × 2 × 2 kombinace, platné jsou jen některétype State = { isLoading: boolean; error?: string; data?: User[] }// ✅ Jen čtyři platné stavy; TS v každé větvi ví, co je k dispozicitype State =| { status: 'idle' }| { status: 'loading' }| { status: 'error'; error: string }| { status: 'success'; data: User[] }switch (state.status) {case 'success': return <UserList users={state.data} /> // data tu určitě jsoucase 'error': return <p>{state.error}</p>}
Krátký tahák k syntaxi:
| Zápis | Význam | Příklad |
|---|---|---|
: | typ hodnoty (proměnná, parametr, návratová hodnota) | function f(id: string): User |
<…> | typový argument generiky | useState<User | null>(null) |
| nic | nech TS odvodit – kde to jde, je to nejlepší volba | const [n, setN] = useState(0) |
satisfies | ověří typ, ale zachová přesný tvar | const routes = {…} satisfies Routes |
as | tvrzení „věř mi“ – žádná kontrola za běhu, používej minimálně | el as HTMLInputElement |
// Props, children a události v Reactutype ButtonProps = React.ComponentProps<'button'> & { variant?: 'primary' | 'ghost' }function Panel({ title, children }: { title: string; children: React.ReactNode }) { … }// Typ události nemusíš psát, když je handler inline – TS ho odvodí<input onChange={(e) => setName(e.target.value)} />// Generická komponenta: vztah mezi items a renderItem drží typ Tfunction List<T>({ items, renderItem }: { items: T[]; renderItem: (item: T) => React.ReactNode }) { … }
📚 Podrobně v lekci Kam napsat typ.
📚 Podrobně v lekci TypeScript s Reactem.
Všechno, co má přežít obnovení stránky, tlačítko Zpět nebo poslání odkazu kolegovi, patří do URL – ne do useState. Typicky: aktuální stránka, ID detailu, filtry, řazení, vyhledávání, otevřená záložka.
// ❌ Filtr se po reloadu ztratí a nejde nasdíletconst [category, setCategory] = useState('all')// ✅ Filtr v search params: /produkty?kategorie=boty&razeni=cenaconst [params, setParams] = useSearchParams()const category = params.get('kategorie') ?? 'all'const changeCategory = (c: string) =>setParams((prev) => { prev.set('kategorie', c); return prev })
/produkty/:id) identifikují co se zobrazuje, search params upravují jak.<Outlet /> sdílí layout; loader a error element platí jen pro svoji část stránky.<Link>), navigate() jen po akci (např. po uložení).📚 Podrobně v lekci Routování (React Router).
Data ze serveru nejsou tvůj stav – jsou to kopie něčeho, co patří serveru a může se tam kdykoli změnit. Potřebují cachování, deduplikaci požadavků, opakování při chybě, obnovení po návratu do okna a invalidaci po změně. Ručně v useEffect to napíšeš špatně; proto existuje TanStack Query (a podobné knihovny).
// ✅ queryKey obsahuje VŠECHNO, na čem dotaz závisí → změna = nový dotaz/cacheconst { data, isPending, error } = useQuery({queryKey: ['products', { category, page }],queryFn: () => api.products({ category, page }),staleTime: 60_000, // minutu považuj data za čerstvá})// ✅ Po změně na serveru zneplatni dotčená data, UI se obnoví samoconst queryClient = useQueryClient()const addProduct = useMutation({mutationFn: api.createProduct,onSuccess: () => queryClient.invalidateQueries({ queryKey: ['products'] }),})
📚 Podrobně v lekci Načítání dat.
Principy se nejlíp uplatní, když je projdeš v tomhle pořadí – přesně tak vznikal i závěrečný projekt:
| Situace | Sáhni po… | Ne po… |
|---|---|---|
| Hodnota jde spočítat z jiných dat | výpočtu při renderu | useState + useEffect |
| Uživatel klikl a má se něco stát | kódu v handleru | efektu sledujícím příznak |
| Napojení na WebSocket, timer, DOM API | useEffect s cleanupem | kódu přímo v renderu |
| Hodnota, kterou nevidí uživatel (ID timeru) | useRef | useState |
| Mnoho různých změn jednoho stavu | useReducer | pěti setterů v handlerech |
| Téma / uživatel potřebný hluboko ve stromu | Contextu | prop drillingu přes 6 úrovní |
| Filtr, který má přežít reload | search params | useState |
| Data z API | TanStack Query | fetch v useEffect |
| Pomalý render | Profileru, pak posunutí stavu dolů | memo všude „pro jistotu“ |
| Komponenta s 15 props | kompozice přes children | dalšího booleanu |
products ve stavu a potřebuješ zobrazit celkovou cenu. Co uděláš?