Skip to content

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)

  1. Volej je jen na nejvyšší úrovni komponenty, ne v if, for, while ani uvnitř funkcí
  2. Volej je jen z React komponent nebo vlastních hooků (funkce začínající use)
  3. 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 useState byl 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-hooks tohle 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 type

Immutability

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í:

  1. Před dalším spuštěním efektu (závislost se změnila)
  2. 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. AbortController to ř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

ChybaDůsledekŘešení
Chybějící závislostZastaralé hodnoty, "stale closure"Přidat do pole závislostí
Vynechané pole závislostíSpustí se po každém renderuPřidat [] nebo [deps]
setState v efektu bez podmínky a bez depsNekonečná smyčkaPřidat deps + podmínku
Forgotten cleanup u timeru/listeneruMemory leakVrátit funkci z efektu
Async funkce přímo jako argumentChyba (efekt nechce Promise)Wrap: useEffect(() => { (async () => {...})() }, [])
Závislost na objektu/poliSpustí 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 useRef místo useState.

useState vs useRef

useStateuseRef
Spouští re-renderAnoNe
Hodí se proUI hodnoty (text, počet, pole)Timery, DOM odkazy, neviditelné hodnoty
Přístupstateref.current
ZměnasetState(...)ref.current = ... (přímo)
Persist mezi renderyAnoAno

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 useId pro key v map()! Má sloužit pro accessibility/HTML id, ne pro identifikaci prvků v listu. Pro key použ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. useId je 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)  ← UNMOUNT

Třídní ekvivalent (pro představu, nepoužívat):

  • useEffect(() => {}, [])componentDidMount
  • Cleanup function ≈ componentWillUnmount
  • useEffect(() => {}, [x])componentDidUpdate (s kontrolou x)

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)

HookK čemu
useStateLokální stav
useEffectVedlejší efekty (API, timer, DOM)
useRefDOM odkaz, mutable hodnota bez re-renderu
useIdUnikátní ID pro accessibility
useContextČtení sdíleného Context
useMemoZapamatovaný výpočet (memoizace)
useCallbackZapamatovaná funkce (referenční stabilita)
useReducerKomplexní stav (alternativa k useState, jako Redux mini)
useTransitionMarkování updatů jako "non-urgent" (React 18+)
useDeferredValueOdložená hodnota pro performance (React 18+)

Časté chyby s hooks

ChybaDůsledekŘešení
Hook v if nebo cykluCrash při re-renderuVždy na top-level
Zapomenutá závislost v useEffectStale closurePřidat do deps array
Vynechané pole závislostíSpustí se po každém renderu[] nebo [deps]
setState v efektu bez podmínkyNekonečná smyčkaPřidat deps + podmínku
Forgotten cleanup u timeruMemory leak, dva timery v StrictModeVrátit cleanup
useRef pro UI hodnotuNepřekreslí sePoužít useState
useState pro DOM refNadbytečné re-renderyPoužít useRef
useEffect pro derived stateZbytečný re-renderSpočítat v renderu nebo useMemo
useId jako key v mapŠpatné chováníPoužít vlastní ID z dat
Math.random() v renderuZměna při každém renderuuseRef 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:

  • useState drží formulářová pole
  • useId propojí <label> a <input>
  • useRef pro automatický focus na první input
  • useEffect s 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 dev

Datový 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:

#ÚčelZávislostiCleanup
1Focus na input[]Ne
2Fetch nápojů[]clearTimeout
3Load draft z localStorage[]Ne
4Auto-save do localStorage[obj]Ne
5Update 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 tebe

Bonus 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

PojemRychlá odpověď
HookFunkce 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í
useStateLokální stav, změna spustí re-render
useEffectVedlejší 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 funkceVrácená funkce v useEffect, spustí se před dalším efektem nebo unmountu
useRefMutable container {current} bez re-renderu, DOM odkaz
useIdStabilní unikátní ID pro accessibility
StrictModeDev-only, spouští useEffect 2x pro odhalení bugů s cleanupem
Vlastní hookFunkce začínající use, sdílí logiku mezi komponentami
Funkční updatesetState(prev => prev + 1) místo setState(state + 1)
ImmutabilityVytvořit nový objekt/pole, nemodifikovat přímo
Stale closureFunkce v efektu si pamatuje starou hodnotu (chybí v deps)
AbortControllerAPI pro zrušení fetch requestů, prevence race condition
Derived stateVypočítané z props/state v renderu (ne přes useEffect)
useState vs useRefuseState = UI, re-render. useRef = DOM/neviditelné, bez re-renderu

Časté chytáky

OtázkaOdpověď
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)
  • useRef použitý mimo useEffect (DOM ještě neexistuje při prvním renderu)
  • Math.random() pro id (neguje accessibility, mění se při renderu)
  • setObj(...) co modifikuje původní objekt (obj.jmeno = '...')
  • useId použit jako key v map (š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 form onSubmit (form reloaduje stránku)
  • Zapomenutý try-catch u JSON.parse(localStorage.getItem(...)) (crash při poškozených datech)
  • useState pro DOM ref (zbytečné re-rendery)
  • useRef pro 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š)