Vzhled
23 • React - hooks
React hooks: useId, useState, useRef, useEffect
Formát: 30 min praktická úloha, 15 min obhajoba + teorie. Podle informací z minulých let bude praktika doplnit
useEffect()do existujícího projektu. Připraveno na to. Hlavní focus teorie:useEffect(závislosti, cleanup, časté chyby).
Část 1: Teorie
Co jsou hooks a proč existují
Hook je speciální funkce začínající use, která dává funkcionálním komponentám schopnosti, které dříve uměly jen třídní komponenty (stav, lifecycle, refy a další).
tsx
import { useState, useEffect, useRef, useId } from 'react';Bez hooků by funkcionální komponenta byla jen čistá funkce vracející JSX. Neuměla by si pamatovat hodnoty mezi rendery, sledovat změny ani komunikovat s DOM.
Hooks byly přidány v React 16.8 (únor 2019) a postupně vytlačily třídní komponenty. Dnes je standardní psát všechno funkcionálně.
Pravidla hooků (důležité, často chyták)
- Volej je jen na nejvyšší úrovni komponenty, ne v
if,for,whileani uvnitř funkcí - Volej je jen z React komponent nebo vlastních hooků (funkce začínající
use) - Pořadí volání musí být stejné při každém renderu, React je interně identifikuje pořadím, ne názvem
tsx
// ❌ ŠPATNĚ
function Komponenta({ prihlasen }: { prihlasen: boolean }) {
if (prihlasen) {
const [data, setData] = useState(null); // podmíněný hook!
}
}
// ✓ SPRÁVNĚ
function Komponenta({ prihlasen }: { prihlasen: boolean }) {
const [data, setData] = useState(null); // vždy zavolán
if (prihlasen) {
// logika podmíněná, ale hook ne
}
}Proč to platí: React si vede interní seznam hooks pro každou komponentu. Identifikuje je podle pořadí, ne názvu. Pokud by
useStatebyl volán někdy a někdy ne, React by si spletl, který stav patří kterému hooku, a všechno by se rozsypalo.
Lint plugin
eslint-plugin-react-hookstohle hlídá automaticky, ale ne každý projekt ho má.
useState: lokální stav komponenty
Drží hodnotu, která přežije mezi rendery. Změna spustí re-render.
tsx
import { useState } from 'react';
function Pocitadlo() {
const [pocet, setPocet] = useState<number>(0);
return (
<div>
<p>Klikuto: {pocet}×</p>
<button onClick={() => setPocet(pocet + 1)}>+1</button>
<button onClick={() => setPocet(0)}>Reset</button>
</div>
);
}Funkční update (závisí na předchozí hodnotě)
tsx
// ❌ Riziko race condition při více rychlých kliknutích
setPocet(pocet + 1);
// ✓ Garantovaně použije aktuální hodnotu
setPocet(prev => prev + 1);Když máš třeba 3 rychlá kliknutí během jednoho renderu, pocet + 1 třikrát použije stejnou (zastaralou) hodnotu. Funkční update řeší tím, že React předá aktuální stav.
Typované state
tsx
const [text, setText] = useState(''); // string (odvozeno)
const [pocet, setPocet] = useState(0); // number (odvozeno)
const [aktiv, setAktiv] = useState(false); // boolean (odvozeno)
const [polozky, setPolozky] = useState<string[]>([]); // explicitní pole
const [user, setUser] = useState<User | null>(null); // nullable
const [filter, setFilter] = useState<'vse' | 'aktivni'>('vse'); // union typeImmutability
State nikdy nemodifikuj přímo, vždy přes setter, vždy s novou referencí:
tsx
// ❌ React nezjistí změnu (stejná reference)
polozky.push('nová');
setPolozky(polozky);
// ✓ Nové pole
setPolozky([...polozky, 'nová']);
// ❌ Modifikace objektu
obj.jmeno = 'axo';
setObj(obj);
// ✓ Nový objekt
setObj({ ...obj, jmeno: 'axo' });useEffect: vedlejší efekty (klíčové)
Spustí kód po vykreslení komponenty. Slouží pro "věci mimo React":
- API volání (fetch, axios)
- Timery a intervaly (setInterval, setTimeout)
- Manipulace s DOM (focus, scroll, document.title)
- Předplatné na události (window.addEventListener)
- Čtení/zápis do
localStorage - Synchronizace s externími systémy
Základní syntaxe
tsx
useEffect(() => {
// KÓD EFEKTU: spustí se po renderu
console.log('Komponenta vykreslena');
return () => {
// CLEANUP (volitelný): spustí se před dalším efektem nebo při unmount
console.log('Cleanup');
};
}, [zavislosti]); // pole závislostí: určuje KDY se efekt spustíZávislosti: kdy se efekt spustí
| Pole závislostí | Kdy se efekt spustí |
|---|---|
[] (prázdné) | Jen jednou po prvním renderu (mount) |
[a, b] | Po prvním renderu + pokaždé když se změní a nebo b |
| (vynecháno) | Po každém renderu, pozor: může způsobit nekonečnou smyčku! |
tsx
// Jen při mount
useEffect(() => {
console.log('Mount');
}, []);
// Při mount + při změně x
useEffect(() => {
console.log('x se změnil:', x);
}, [x]);
// Po každém renderu (zřídka chcete)
useEffect(() => {
console.log('Každý render');
});Typické use cases useEffect
A) Načtení dat z API
tsx
function ProfilUzivatele({ id }: { id: number }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetch(`/api/users/${id}`)
.then(res => res.json())
.then(data => setUser(data));
}, [id]); // znovu při změně id
if (!user) return <p>Načítám...</p>;
return <h1>{user.jmeno}</h1>;
}B) Změna document.title
tsx
useEffect(() => {
document.title = `Kliknutí: ${pocet}`;
}, [pocet]);C) Timer / interval (s cleanupem!)
tsx
useEffect(() => {
const id = setInterval(() => {
console.log('Tick');
}, 1000);
return () => clearInterval(id); // CLEANUP, jinak by tikala víckrát
}, []);D) Event listener na window (s cleanupem!)
tsx
useEffect(() => {
const handler = () => setSirka(window.innerWidth);
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);E) Synchronizace s localStorage
tsx
// Načtení při mount
useEffect(() => {
const saved = localStorage.getItem('text');
if (saved) setText(saved);
}, []);
// Uložení při každé změně
useEffect(() => {
localStorage.setItem('text', text);
}, [text]);Cleanup function
Vrácená funkce v useEffect se spustí:
- Před dalším spuštěním efektu (závislost se změnila)
- Při odmontování komponenty (zmizí z DOM)
Použití: zrušit timer, odregistrovat listener, zrušit fetch. Předejít memory leakům.
Race condition s AbortController
Klasický problém: uživatel rychle změní
id, první fetch ještě běží, druhý začne, vrátí se v opačném pořadí, vidíš data od staré requestu.AbortControllerto řeší.
tsx
useEffect(() => {
const ctrl = new AbortController();
fetch(`/api/users/${id}`, { signal: ctrl.signal })
.then(res => res.json())
.then(setUser)
.catch(err => {
if (err.name !== 'AbortError') console.error(err);
});
return () => ctrl.abort(); // zruší fetch pokud id se změní nebo komponenta zmizí
}, [id]);Časté chyby s useEffect
| Chyba | Důsledek | Řešení |
|---|---|---|
| Chybějící závislost | Zastaralé hodnoty, "stale closure" | Přidat do pole závislostí |
| Vynechané pole závislostí | Spustí se po každém renderu | Přidat [] nebo [deps] |
setState v efektu bez podmínky a bez deps | Nekonečná smyčka | Přidat deps + podmínku |
| Forgotten cleanup u timeru/listeneru | Memory leak | Vrátit funkci z efektu |
| Async funkce přímo jako argument | Chyba (efekt nechce Promise) | Wrap: useEffect(() => { (async () => {...})() }, []) |
| Závislost na objektu/poli | Spustí se při každém renderu (nová reference) | useMemo nebo primitivní hodnoty v deps |
tsx
// ❌ Chybí závislost (eslint-plugin-react-hooks to chytí)
useEffect(() => {
console.log(pocet);
}, []);
// ❌ Nekonečná smyčka
useEffect(() => {
setPocet(pocet + 1); // bez závislostí → po každém renderu
});
// ❌ Async funkce přímo
useEffect(async () => { // chyba! useEffect nechce Promise
const data = await fetch(...);
}, []);
// ✓ Závislosti kompletní
useEffect(() => {
console.log(pocet);
}, [pocet]);
// ✓ Async wrapper
useEffect(() => {
(async () => {
const data = await fetch(...);
})();
}, []);Kdy NEPOUŽÍVAT useEffect (anti-patterny)
Tahle sekce v běžné teorii často chybí, ale zkoušející ji ocení. Hodí se i do budoucnosti do praxe.
useEffect má tendenci být overused. React tým má dokumentaci "You Might Not Need an Effect".
Anti-pattern 1: Derived state z props
tsx
// ❌ Zbytečný useEffect
function Komponenta({ jmeno }: { jmeno: string }) {
const [velkePisme, setVelkePisme] = useState('');
useEffect(() => {
setVelkePisme(jmeno.toUpperCase());
}, [jmeno]);
return <h1>{velkePisme}</h1>;
}
// ✓ Prostě počítej během renderu
function Komponenta({ jmeno }: { jmeno: string }) {
const velkePisme = jmeno.toUpperCase(); // přepočítává se každý render, ale je to OK
return <h1>{velkePisme}</h1>;
}Anti-pattern 2: Reset stavu při změně props
tsx
// ❌ useEffect na reset
useEffect(() => {
setSelectedId(null);
}, [userId]);
// ✓ Použij key prop (komponenta se znovu mountuje)
<ProfilDetail key={userId} userId={userId} />Anti-pattern 3: Vypočítat odvozená data s memoization
tsx
// ❌ useEffect pro filtrování
const [filtrovane, setFiltrovane] = useState([]);
useEffect(() => {
setFiltrovane(seznam.filter(x => x.aktivni));
}, [seznam]);
// ✓ useMemo (nebo prostě v renderu)
const filtrovane = useMemo(() => seznam.filter(x => x.aktivni), [seznam]);Pravidlo: pokud lze hodnotu spočítat z props/stavu během renderu, nepotřebuješ useEffect. useEffect je pro synchronizaci s externími systémy (DOM, API, timer, browser API).
React StrictMode: useEffect běží 2x v dev
Drobnost, na kterou každý nový React developer narazí.
V React 18+ při zapnutém <React.StrictMode> (defaultně v Vite/Next.js dev modu) React úmyslně spouští useEffect dvakrát, aby odhalil bugy s missing cleanupem.
tsx
useEffect(() => {
console.log('Effect run'); // v dev modu se vypíše DVAKRÁT
return () => console.log('Cleanup');
}, []);
// V dev console:
// "Effect run"
// "Cleanup"
// "Effect run"
// V production běží jen jednou.Klasický bug, který tohle odhalí: bez cleanup u setInterval se v dev modu rovnou rozjedou dva intervaly. V production sice jen jeden, ale stejně by tikalo špatně při změně závislostí.
Závěr: vždy piš správný cleanup. StrictMode tě k tomu donutí.
useRef: odkaz, který přežije rendery
Vrací mutovatelný kontejner ({ current: ... }), jehož změna NEspouští re-render. Dvě hlavní použití:
A) Přístup k DOM elementu
tsx
import { useRef, useEffect } from 'react';
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus(); // při mount nastaví focus na input
}, []);
return <input ref={inputRef} />;
}Klíčové: ref={inputRef} napojí ref na DOM element. inputRef.current je pak ten samotný <input> HTML element.
B) Uchování hodnoty bez re-renderu
tsx
function Casovac() {
const [cas, setCas] = useState(0);
const intervalRef = useRef<number | null>(null); // uchová ID intervalu
const start = () => {
intervalRef.current = window.setInterval(() => {
setCas(prev => prev + 1);
}, 1000);
};
const stop = () => {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
return (
<div>
<p>Čas: {cas}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}ID intervalu nepotřebuje vyvolat re-render při změně, jen si ho potřebujeme zapamatovat. Proto
useRefmístouseState.
useState vs useRef
useState | useRef | |
|---|---|---|
| Spouští re-render | Ano | Ne |
| Hodí se pro | UI hodnoty (text, počet, pole) | Timery, DOM odkazy, neviditelné hodnoty |
| Přístup | state | ref.current |
| Změna | setState(...) | ref.current = ... (přímo) |
| Persist mezi rendery | Ano | Ano |
useId: unikátní ID pro accessibility
Vygeneruje stabilní unikátní ID, ideální pro propojení <label> a <input> v re-použitelných komponentách.
tsx
import { useId } from 'react';
function FormPole({ label }: { label: string }) {
const id = useId(); // např. ":r1:"
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type="text" />
</div>
);
}
// Použití: každá instance dostane vlastní unikátní id
<FormPole label="Jméno" />
<FormPole label="Email" />Nepoužívej
useIdprokeyvmap()! Má sloužit pro accessibility/HTML id, ne pro identifikaci prvků v listu. Prokeypoužij vlastní stabilní ID z dat.
Proč ne
Math.random()? Náhodné ID by se měnilo při každém renderu, label by ztratil propojení s inputem.useIdje stabilní mezi rendery a navíc safe pro SSR (server a klient generují stejné ID).
Lifecycle funkcionální komponenty s hooks
1× MOUNT
─────────
[Komponenta vznikne]
│
▼
Render JSX
│
▼
useEffect(() => {...}, []) ← spustí se JEDNOU
│
▼
┌──────────────────────────┐
│ Komponenta žije │
│ │
│ setState() → re-render │
│ │
│ useEffect(..., [dep]) │
│ spustí se když se dep │
│ změnila │
│ ──────────────── │
│ Cleanup z předchozí │
│ iterace efektu │
│ (před novým spuštěním) │
└──────────────────────────┘
│
▼
[Komponenta zmizí]
│
▼
Cleanup funkce (return v useEffect) ← UNMOUNTTřídní ekvivalent (pro představu, nepoužívat):
useEffect(() => {}, [])≈componentDidMount- Cleanup function ≈
componentWillUnmount useEffect(() => {}, [x])≈componentDidUpdate(s kontroloux)
Vlastní hook: sdílení logiky mezi komponentami
Funkce začínající use*, která uvnitř volá další hooks.
tsx
import { useState, useEffect } from 'react';
// Vlastní hook pro localStorage state
function useLocalStorage<T>(key: string, vychozi: T) {
const [hodnota, setHodnota] = useState<T>(() => {
const ulozeno = localStorage.getItem(key);
return ulozeno ? JSON.parse(ulozeno) : vychozi;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(hodnota));
}, [key, hodnota]);
return [hodnota, setHodnota] as const;
}
// Použití: funguje úplně stejně jako useState
const [text, setText] = useLocalStorage<string>('muj-text', '');
as constříká TypeScriptu, že vrácený tuple má pevné typy[T, Setter<T>], ne obecný array.
Vlastní hooks musí začínat
use, aby React linter rozpoznal, že jsou hook a aplikoval pravidla.
Další běžné hooky (přehled)
| Hook | K čemu |
|---|---|
useState | Lokální stav |
useEffect | Vedlejší efekty (API, timer, DOM) |
useRef | DOM odkaz, mutable hodnota bez re-renderu |
useId | Unikátní ID pro accessibility |
useContext | Čtení sdíleného Context |
useMemo | Zapamatovaný výpočet (memoizace) |
useCallback | Zapamatovaná funkce (referenční stabilita) |
useReducer | Komplexní stav (alternativa k useState, jako Redux mini) |
useTransition | Markování updatů jako "non-urgent" (React 18+) |
useDeferredValue | Odložená hodnota pro performance (React 18+) |
Časté chyby s hooks
| Chyba | Důsledek | Řešení |
|---|---|---|
Hook v if nebo cyklu | Crash při re-renderu | Vždy na top-level |
Zapomenutá závislost v useEffect | Stale closure | Přidat do deps array |
| Vynechané pole závislostí | Spustí se po každém renderu | [] nebo [deps] |
setState v efektu bez podmínky | Nekonečná smyčka | Přidat deps + podmínku |
| Forgotten cleanup u timeru | Memory leak, dva timery v StrictMode | Vrátit cleanup |
useRef pro UI hodnotu | Nepřekreslí se | Použít useState |
useState pro DOM ref | Nadbytečné re-rendery | Použít useRef |
| useEffect pro derived state | Zbytečný re-render | Spočítat v renderu nebo useMemo |
useId jako key v map | Špatné chování | Použít vlastní ID z dat |
Math.random() v renderu | Změna při každém renderu | useRef nebo useId |
Část 2: Praktická úloha
Co může praktická úloha obsahovat
Podle informací z minulých let bude úloha typicky doplnit useEffect (a další hooks) do existujícího starter projektu. To znamená:
- Vyplnit kostru s několika TODO komentáři
- Různé typy useEffect s různými závislostmi (
[],[x],[obj]) - Cleanup funkce (clearTimeout, removeEventListener)
- useRef pro auto-focus
- useId pro form accessibility
- useState pro form data
Příklad zadání: Formulář pro kavárnu
Vytvoříš objednávkový formulář kavárny, který procvičí všechny 4 hooky:
useStatedrží formulářová poleuseIdpropojí<label>a<input>useRefpro automatický focus na první inputuseEffects 5 různými použitími (focus, fetch, auto-save, load draft, document.title)
Setup
bash
npm create vite@latest kavarna -- --template react-ts
cd kavarna
npm install
npm run devDatový model
tsx
type Objednavka = {
jmeno: string;
napoj: string;
mnozstvi: number;
poznamka: string;
};Řešení: kompletní App.tsx
tsx
import { useState, useEffect, useRef, useId } from 'react';
type Objednavka = {
jmeno: string;
napoj: string;
mnozstvi: number;
poznamka: string;
};
const STORAGE_KEY = 'objednavka-draft';
export default function App() {
// Stav formuláře (jeden useState pro celý objekt)
const [obj, setObj] = useState<Objednavka>({
jmeno: '',
napoj: '',
mnozstvi: 1,
poznamka: ''
});
// Seznam dostupných nápojů (načteme z fake API)
const [napoje, setNapoje] = useState<string[]>([]);
// Stav pro počet uložení (do title)
const [pocetOdeslani, setPocetOdeslani] = useState(0);
// Ref pro automatický focus na první input
const jmenoRef = useRef<HTMLInputElement>(null);
// useId pro accessibility (label htmlFor + input id)
const jmenoId = useId();
const napojId = useId();
const mnozstviId = useId();
const poznamkaId = useId();
// ═══ TODO 1: useEffect: focus na první input PŘI MOUNT ═══
useEffect(() => {
jmenoRef.current?.focus();
}, []);
// ═══ TODO 2: useEffect: načti napoje z fake API PŘI MOUNT ═══
useEffect(() => {
const id = setTimeout(() => {
setNapoje(['Espresso', 'Cappuccino', 'Latte', 'Čaj']);
}, 500);
// CLEANUP: zruš timer pokud komponenta zmizí
return () => clearTimeout(id);
}, []);
// ═══ TODO 3: useEffect: načti DRAFT z localStorage PŘI MOUNT ═══
useEffect(() => {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
try {
setObj(JSON.parse(saved));
} catch {
// poškozený JSON ignoruj
}
}
}, []);
// ═══ TODO 4: useEffect: auto-save objednávky do localStorage ═══
// POZOR: spustí se i při mountu, takže poprvé zapíše defaultní hodnoty.
// To je OK (přepíšeme draft který se mezitím načetl v TODO 3,
// ale TODO 3 se spustí dřív, takže nedojde ke ztrátě).
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(obj));
}, [obj]);
// ═══ TODO 5: useEffect: document.title podle počtu odeslání ═══
useEffect(() => {
document.title = `Kavárna (odesláno: ${pocetOdeslani})`;
}, [pocetOdeslani]);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log('Objednávka:', obj);
setPocetOdeslani(prev => prev + 1);
// Reset formuláře
setObj({ jmeno: '', napoj: '', mnozstvi: 1, poznamka: '' });
localStorage.removeItem(STORAGE_KEY);
};
return (
<form onSubmit={handleSubmit} style={{ maxWidth: 500, margin: '2rem auto' }}>
<h1>Objednávka: Kavárna ☕</h1>
<div style={{ marginBottom: '1rem' }}>
<label htmlFor={jmenoId}>Jméno:</label>
<input
ref={jmenoRef}
id={jmenoId}
value={obj.jmeno}
onChange={e => setObj({ ...obj, jmeno: e.target.value })}
required
/>
</div>
<div style={{ marginBottom: '1rem' }}>
<label htmlFor={napojId}>Nápoj:</label>
<select
id={napojId}
value={obj.napoj}
onChange={e => setObj({ ...obj, napoj: e.target.value })}
required
>
<option value="">-- Vyberte --</option>
{napoje.map(n => (
<option key={n} value={n}>{n}</option>
))}
</select>
</div>
<div style={{ marginBottom: '1rem' }}>
<label htmlFor={mnozstviId}>Množství:</label>
<input
id={mnozstviId}
type="number"
min={1}
value={obj.mnozstvi}
onChange={e => setObj({ ...obj, mnozstvi: Number(e.target.value) })}
/>
</div>
<div style={{ marginBottom: '1rem' }}>
<label htmlFor={poznamkaId}>Poznámka:</label>
<textarea
id={poznamkaId}
value={obj.poznamka}
onChange={e => setObj({ ...obj, poznamka: e.target.value })}
/>
</div>
<button type="submit">Objednat</button>
</form>
);
}Co se v řešení děje
5 různých useEffect s různými závislostmi:
| # | Účel | Závislosti | Cleanup |
|---|---|---|---|
| 1 | Focus na input | [] | Ne |
| 2 | Fetch nápojů | [] | clearTimeout |
| 3 | Load draft z localStorage | [] | Ne |
| 4 | Auto-save do localStorage | [obj] | Ne |
| 5 | Update document.title | [pocetOdeslani] | Ne |
TODO 1 (focus): useRef<HTMLInputElement>(null) vytvoří ref. <input ref={jmenoRef}> ho napojí na DOM element. useEffect s [] (jen při mount) zavolá .focus().
TODO 2 (fetch napojů): Simulace fetch přes setTimeout (500ms delay). Cleanup s clearTimeout zruší timer, pokud uživatel zavře stránku během 500ms (jinak by setNapoje zavolaný na unmounted komponentě hodil warning).
TODO 3 (load draft): Při mount zkusíme načíst draft z localStorage. Try-catch chrání proti poškozenému JSON. Pořadí spuštění je důležité: TODO 3 musí být před TODO 4 v kódu, aby se draft načetl dřív než ho TODO 4 přepíše defaultními hodnotami.
TODO 4 (auto-save): Závislost [obj] znamená, že efekt se spustí pokaždé, když se objekt změní (uživatel napsal něco do inputu). Také se spustí při mount (uloží defaultní hodnoty). Při refreshi se draft natáhne z TODO 3.
TODO 5 (document.title): Změní document.title po odeslání. Vidí se v záložce prohlížeče.
Form pattern: jeden velký useState objekt místo 4 samostatných stavů. Update přes setObj({ ...obj, jmeno: e.target.value }) (spread + override jednoho pole).
Accessibility: useId generuje stabilní ID pro každý input. <label htmlFor={id}> a <input id={id}> musí mít stejné ID, aby kliknutí na label fokuslo input (a screen reader věděl, co je popisek čeho).
Bonusy
Bonus A: useEffect s cleanup pro window resize
tsx
const [sirka, setSirka] = useState(window.innerWidth);
useEffect(() => {
const handler = () => setSirka(window.innerWidth);
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);
// V JSX:
<p>Šířka okna: {sirka}px</p>Bonus B: Vlastní hook useLocalStorage
tsx
function useLocalStorage<T>(key: string, vychozi: T): [T, (val: T) => void] {
const [hodnota, setHodnota] = useState<T>(() => {
const ulozeno = localStorage.getItem(key);
if (!ulozeno) return vychozi;
try {
return JSON.parse(ulozeno);
} catch {
return vychozi;
}
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(hodnota));
}, [key, hodnota]);
return [hodnota, setHodnota];
}
// Použití: sjednotí TODO 3 + TODO 4
const [obj, setObj] = useLocalStorage<Objednavka>('objednavka-draft', {
jmeno: '', napoj: '', mnozstvi: 1, poznamka: ''
});
// TODO 3 a TODO 4 můžeš smazat, hook to dělá za tebeBonus C: useEffect se závislostí na obj.napoj
tsx
useEffect(() => {
if (obj.napoj) {
console.log('Vybráno:', obj.napoj);
}
}, [obj.napoj]); // spustí se jen při změně napoje, ne ostatních políBonus D: AbortController pro fetch napojů (race condition prevention)
Pokud bys nahradil setTimeout reálným fetch:
tsx
useEffect(() => {
const ctrl = new AbortController();
fetch('/api/napoje', { signal: ctrl.signal })
.then(r => r.json())
.then(setNapoje)
.catch(err => {
if (err.name !== 'AbortError') console.error(err);
});
return () => ctrl.abort();
}, []);Část 3: Tipy pro obhajobu
Co u obhajoby říct
"V zadání jsem doplnil 5 useEffect hooks s různými závislostmi. Pro mount-only chování použiju prázdné pole závislostí []. Pro reakci na změnu specifické hodnoty dám tu hodnotu do pole. Cleanup u setTimeout je důležitý kvůli StrictMode: React 18 v dev modu spouští efekt 2x pro odhalení memory leaků. Bez clearTimeout by mi šly dva timery zároveň. Pro focus jsem použil useRef typu HTMLInputElement, který se napojil přes ref atribut na input. useRef se hodí pro DOM přístup, protože nezpůsobí re-render. Pro accessibility jsem použil useId, který vygeneruje stabilní unikátní ID pro propojení label htmlFor a input id. Math.random() by tu nefungoval, protože by se ID měnilo při každém renderu a label by ztratil propojení. Stav formuláře držím v jednom useState objektu, update přes spread operator pro immutability."
Klíčové pojmy pro teorii
| Pojem | Rychlá odpověď |
|---|---|
| Hook | Funkce začínající use, dává funkcionálním komponentám stav a lifecycle |
| Pravidla hooků | Top-level only, jen v komponentě nebo vlastním hooku, stejné pořadí |
useState | Lokální stav, změna spustí re-render |
useEffect | Vedlejší efekty (API, timer, DOM, listener) |
| Pole závislostí | Řídí kdy efekt běží: [] = jednou, [x] = při změně x, vynecháno = každý render |
| Cleanup funkce | Vrácená funkce v useEffect, spustí se před dalším efektem nebo unmountu |
useRef | Mutable container {current} bez re-renderu, DOM odkaz |
useId | Stabilní unikátní ID pro accessibility |
| StrictMode | Dev-only, spouští useEffect 2x pro odhalení bugů s cleanupem |
| Vlastní hook | Funkce začínající use, sdílí logiku mezi komponentami |
| Funkční update | setState(prev => prev + 1) místo setState(state + 1) |
| Immutability | Vytvořit nový objekt/pole, nemodifikovat přímo |
| Stale closure | Funkce v efektu si pamatuje starou hodnotu (chybí v deps) |
| AbortController | API pro zrušení fetch requestů, prevence race condition |
| Derived state | Vypočítané z props/state v renderu (ne přes useEffect) |
useState vs useRef | useState = UI, re-render. useRef = DOM/neviditelné, bez re-renderu |
Časté chytáky
| Otázka | Odpověď |
|---|---|
| Co jsou hooks? | Funkce začínající use, dávají funkcionálním komponentám stav, lifecycle a další schopnosti. |
| Pravidla hooků? | Vždy na top-level (ne v if/cyklu), jen v komponentě nebo vlastním hooku. React je identifikuje pořadím. |
Co useEffect s []? | Spustí se jen jednou po prvním renderu (mount). Pro inicializaci, fetch dat. |
Co useEffect s [x]? | Spustí se při mount + při každé změně x. |
Co useEffect bez pole? | Spustí se po každém renderu. Pozor, často způsobí nekonečnou smyčku. |
| K čemu cleanup? | Spustí se před dalším efektem nebo při unmount. Pro zrušení timerů, listenerů, fetch. Předchází memory leakům. |
Rozdíl useState a useRef? | useState spouští re-render. useRef ne, je pro DOM odkazy a hodnoty mimo render. |
Proč fokus přes useRef v useEffect? | Při renderu ještě DOM neexistuje. useEffect se spustí po renderu, kdy už ref.current je nastaven. |
K čemu useId? | Pro stabilní unikátní ID v re-usable komponentách (label htmlFor + input id). |
Proč ne Math.random() místo useId? | Náhodné ID se mění při každém renderu, label ztratí propojení. useId je stabilní. |
| Co je StrictMode? | Dev-only wrapper, který spouští useEffect 2x. Odhaluje bugy s missing cleanupem. |
| Co je stale closure? | Efekt si pamatuje starou hodnotu proměnné, protože není v deps. Klasický bug. |
| Kdy NEPOUŽÍVAT useEffect? | Pro derived state z props (spočítej v renderu), pro reset stavu při změně props (použij key). |
Časté chyby v praktické úloze
- Chybějící
[]u useEffectu, který má běžet jen jednou (nekonečná smyčka) - Chybějící cleanup u
setTimeout/setInterval(memory leak, dva timery v StrictMode) useRefpoužitý mimouseEffect(DOM ještě neexistuje při prvním renderu)Math.random()proid(neguje accessibility, mění se při renderu)setObj(...)co modifikuje původní objekt (obj.jmeno = '...')useIdpoužit jakokeyvmap(špatná funkce)- Závislost na objektu
[obj]který má novou referenci každý render (efekt běží pořád) - Async funkce přímo jako
useEffect(async () => {...}, [])(chyba, useEffect nečeká Promise) - Forgotten
e.preventDefault()v formonSubmit(form reloaduje stránku) - Zapomenutý
try-catchuJSON.parse(localStorage.getItem(...))(crash při poškozených datech) useStatepro DOM ref (zbytečné re-rendery)useRefpro UI hodnotu, kterou chci zobrazit (změna se neprojeví)- Více useEffect, které by mohly být v jednom (kosmetika)
- Chybějící order: load draft → auto-save (jinak se default přepíše dřív než načteš)