Lekce 3 · Hooky do hloubky
Efekty (useEffect)
Synchronizace s okolním světem, cleanup, race conditions a kdy efekt NEpotřebuješ.
Načítám lekci…
Lekce 3 · Hooky do hloubky
Synchronizace s okolním světem, cleanup, race conditions a kdy efekt NEpotřebuješ.
Načítám lekci…
Z minulých lekcí víš, že komponenta je kuchař, který podle receptu (props a stav) uvaří jídlo (JSX). Má to jedno přísné pravidlo: během vaření jen vaří. Nevolá dodavatelům, nezapíná rádio, nepíše hostům SMS. Proč? Protože React ho může nechat vařit kdykoli a kolikrát chce – někdy jídlo uvaří a zase zahodí. Kdyby kuchař při každém vaření volal dodavateli, dodavatel by dostal desítky zbytečných objednávek.
Jenže skutečná aplikace s okolním světem mluvit musí: spustit časovač, připojit se k chatu, změnit titulek záložky, poslouchat velikost okna. Na to má React efekty – úkoly, které se provedou až po vaření, když je jídlo na stole (stránka už je vykreslená).
Nejlíp se efekt chápe ne jako „udělej něco jednou“, ale jako udržování souladu. Představ si, že chodíš po bytě a chceš, aby svítilo v místnosti, ve které jsi:
Přesně takhle React efekty spouští: rozsvítí, když se komponenta objeví; zhasne a znovu rozsvítí, když se změní to, na čem efekt závisí; a zhasne naposledy, když komponenta zmizí. Ty jen popíšeš, jak rozsvítit a jak zhasnout.
Chceme, aby se v titulku záložky prohlížeče ukazoval počet nepřečtených zpráv: „(3) Kurz Reactu“. Titulek ale nepatří Reactu – žije v document.title, mimo JSX. Je to tedy „okolní svět“, se kterým se musíme synchronizovat.
function Inbox() {const [unread, setUnread] = useState(0)// ❌ Takhle ne: zápis do okolního světa přímo při vaření// document.title = `(${unread}) Kurz Reactu`// ✅ Efekt: provede se až po vykresleníuseEffect(() => {document.title = `(${unread}) Kurz Reactu`}, [unread])return <button onClick={() => setUnread((u) => u + 1)}>Přišla zpráva</button>}
Co se stane po kliknutí:
setUnread objedná nový render. React zavolá Inbox() – kuchař uvaří nové JSX. Titulku se zatím nikdo nedotkl.[unread]: změnila se hodnota oproti minule? Ano (0 → 1), takže efekt spustí a titulek se přepíše.Funkce předaná do useEffect je tedy „co udělat po vykreslení“. Pole na konci je seznam hodnot, na kterých efekt závisí – podrobně se mu budeme věnovat v kapitole 3.
Co vidíš: Počítadlo nepřečtených zpráv. Efekt ho propisuje do titulku této záložky prohlížeče a při úklidu titulek vrací zpět.
Vyzkoušej:
Co z toho plyne: Stav je v Reactu, titulek mimo něj. Efekt je drží v souladu: po každé změně unread titulek srovná a při odchodu po sobě uklidí.
Načítám interaktivní část…
Některé efekty spustí něco, co běží dál samo: časovač, připojení k serveru, posluchač událostí okna. To je jako rozsvítit světlo – samo nezhasne. Kdo po sobě nezhasne, za měsíc platí velký účet za elektřinu. V aplikaci se tomu říká únik paměti: časovače běží dál, i když jejich komponenta dávno zmizela.
Typický příklad: hodiny, které se každou sekundu aktualizují.
function Clock() {const [now, setNow] = useState(() => new Date())useEffect(() => {// Rozsviť: spusť intervalconst id = setInterval(() => setNow(new Date()), 1000)// Zhasni: funkce, kterou React zavolá při úklidureturn () => clearInterval(id)}, [])return <p>{now.toLocaleTimeString()}</p>}
[] znamená „efekt na ničem nezávisí“. Spustí se jednou, když se hodiny objeví, a úklid proběhne, když zmizí.setNow na komponentě, která už neexistuje. A po každém novém připojení by přibyl další interval.Celý život takového efektu:
| Co se stane | Co udělá React |
|---|---|
| Hodiny se objeví na stránce | Spustí efekt → interval běží. |
| Hodiny se každou sekundu překreslí | Nic – závislosti [] se nezměnily, efekt zůstává. |
| Hodiny zmizí ze stránky | Zavolá úklid → interval se zruší. |
Co vidíš: Hodiny z příkladu výše. Pod nimi je záznam, do kterého efekt píše „▶ spouštím interval“ a úklid „■ ruším interval“.
Vyzkoušej:
Co z toho plyne: Každé „spouštím“ má svoje „ruším“. Díky tomu nikdy neběží dva intervaly naráz, ať se hodiny připojí a odpojí kolikrát chceš.
Načítám interaktivní část…
Vraťme se ke světlu. Závislosti jsou odpověď na otázku „ve které místnosti jsem?“ Dokud jsi ve stejné místnosti, nic nepřepínáš. Když se místnost změní, zhasneš ve staré a rozsvítíš v nové.
Typický příklad: připojení k chatovací místnosti, kterou si uživatel vybírá.
function ChatRoom({ roomId }: { roomId: string }) {useEffect(() => {const connection = connect(roomId) // rozsviť v místnosti roomIdreturn () => connection.disconnect() // zhasni v ní}, [roomId]) // místnost, ve které jsemreturn <h3>Vítej v #{roomId}</h3>}
Uživatel přepne z #obecne na #react:
roomId = "react"."obecne", teď "react" – změna.#obecne.#react.| Zápis | Kdy efekt běží |
|---|---|
useEffect(fn, [a, b]) | Po prvním vykreslení a pokaždé, když se změní a nebo b. Nejčastější. |
useEffect(fn, []) | Jen po prvním vykreslení (a úklid při odpojení). |
useEffect(fn) | Po každém vykreslení. Málokdy to chceš. |
Do pole patří všechno z komponenty, co efekt čte: props, stav a hodnoty z nich spočítané. Není to seznam „kdy chci efekt spustit“, ale „s čím efekt pracuje“. Co se stane, když něco vynecháš:
function Counter() {const [count, setCount] = useState(0)useEffect(() => {const id = setInterval(() => setCount(count + 1), 1000)return () => clearInterval(id)}, []) // ❌ efekt čte count, ale v závislostech není}
Efekt se spustí jednou, při prvním vykreslení, kdy count je 0. Interval si tu nulu nese s sebou (je to snímek z lekce o stavu) a každou sekundu nastaví 0 + 1. Počítadlo zamrzne na 1.
Oprava není „přidat count do závislostí“ (interval by se pak každou sekundu rušil a znovu zakládal), ale změnit kód, aby count nepotřeboval:
useEffect(() => {const id = setInterval(() => setCount((c) => c + 1), 1000) // ✅ updaterreturn () => clearInterval(id)}, []) // teď efekt opravdu nic z komponenty nečte
Večer si objednáš pizzu. Za minutu si to rozmyslíš a objednáš sushi. Sushi přiveze kurýr za dvacet minut, pizzu za čtyřicet. Když nedáš pozor, sníš sushi a pak ti přijde pizza a ty ji sníš taky – i když jsi ji už nechtěl. Moudřejší je říct si: „pizzu jsem zrušil, kdyby přišla, nepřevezmu ji“.
Při načítání dat v efektu je to stejné. Uživatel klikne na Janu, pak hned na Petra. Odešlou se dva požadavky a odpovědi můžou dorazit v libovolném pořadí. Když dorazí Jana až po Petrovi, přepíše ho – a na obrazovce je jiný člověk, než je vybraný.
function Profile({ userId }: { userId: number }) {const [user, setUser] = useState<User | null>(null)useEffect(() => {let ignore = false // „tuhle objednávku ještě chci“fetchUser(userId).then((data) => {if (ignore) return // objednávka zrušená → nepřevezmisetUser(data)})return () => {ignore = true // úklid = zrušení objednávky}}, [userId])}
Projdeme si rychlé přepnutí z Jany (id 1) na Petra (id 2):
| Čas | Co se stane | ignore |
|---|---|---|
| 0 ms | Efekt pro Janu: odešle požadavek 1. | Jana: false |
| 100 ms | Klik na Petra → úklid efektu pro Janu. | Jana: true |
| 100 ms | Efekt pro Petra: odešle požadavek 2. | Petr: false |
| 600 ms | Přijde Petr → ignore je false → zobrazí se. | Petr: false |
| 1200 ms | Přijde Jana → ignore je true → zahodí se. | Jana: true |
Každé spuštění efektu má vlastní proměnnou ignore. Úklid přepne jen tu svoji, takže stará odpověď pozná, že už o ni nikdo nestojí.
Co vidíš: Tlačítka se jmény a karta profilu, která se po kliknutí načte. Falešné API odpovídá s náhodným zpožděním 0,3–1,5 s. Zaškrtávátko zapíná ochranu s ignore z příkladu výše.
Vyzkoušej:
Co z toho plyne: Bez úklidu rozhoduje, kdo dorazí poslední. S úklidem rozhoduje, kdo byl vybraný poslední – a to je to, co uživatel čeká.
Načítám interaktivní část…
Telefonuješ s kamarádem a v půlce hovoru si ztlumíš vyzvánění na další hovory. Kvůli tomu přece nezavěsíš a nevytočíš ho znovu. Ztlumení s probíhajícím hovorem nesouvisí – jen ovlivní, co se stane, když někdo další zavolá.
V efektech nastává stejná situace. Chat se má znovu připojit, když se změní místnost. Když uživatel ztlumí notifikace, připojení měnit nechceme. Ale když přijde zpráva, potřebujeme vědět, jestli je ztlumeno teď. Kdybychom muted dali do závislostí, každé ztlumení by chat odpojilo a připojilo. Kdybychom ho tam nedali, efekt by viděl starou hodnotu.
function ChatRoom({ roomId, muted }: { roomId: string; muted: boolean }) {// Událost efektu: vždy vidí aktuální muted, ale není to závislostconst onMessage = useEffectEvent((msg: string) => {if (!muted) playSound()showNotification(msg)})useEffect(() => {const connection = connect(roomId, onMessage)return () => connection.disconnect()}, [roomId]) // jen místnost – ztlumení připojení nemění}
useEffectEvent z funkce udělá „událost efektu“: kód, který efekt zavolá, ale na který nereaguje. Při každém zavolání vidí nejnovější props a stav.
Co vidíš: Výběr chatovací místnosti a přepínač „Ztlumit notifikace“. Falešné připojení posílá každých 2,5 s zprávu. Záznam ukazuje připojení (🔌), odpojení (❌) a příchozí zprávy (🔔 nebo 🔕).
Vyzkoušej:
Co z toho plyne: Co je v závislostech (místnost), efekt restartuje. Co je v useEffectEvent (ztlumení), efekt jen čte, kdykoli to potřebuje.
Načítám interaktivní část…
Efekty jsou dveře ven z Reactu. Nejčastější chyba je používat je i tam, kde žádné „venku“ není – jako bys chodil z obýváku do kuchyně přes balkon. Jde to, ale je to pomalejší a dřív nebo později spadneš. Než napíšeš useEffect, zeptej se: s jakým systémem mimo React se tu synchronizuju? Když na to nemáš odpověď, efekt nejspíš nepotřebuješ.
// ❌ Efekt dohání stav, který jde spočítatconst [items, setItems] = useState<Item[]>([])const [total, setTotal] = useState(0)useEffect(() => {setTotal(items.reduce((sum, i) => sum + i.price, 0))}, [items])// ✅ Spočítej při vykresleníconst total = items.reduce((sum, i) => sum + i.price, 0)
Efektová verze vykreslí komponentu dvakrát (nejdřív se starým součtem, pak s novým) a na zlomek sekundy ukáže špatné číslo. Žádný vnější systém tu není – jen výpočet.
// ❌ Kliknutí nastaví příznak a efekt na něj „reaguje“const [submitted, setSubmitted] = useState(false)useEffect(() => {if (submitted) {sendOrder(cart)showToast('Objednáno!')}}, [submitted])// ✅ Udělej to rovnou tam, kde událost vzniklafunction handleOrder() {sendOrder(cart)showToast('Objednáno!')}
Objednávka se má odeslat, protože uživatel klikl – ne proto, že se komponenta vykreslila. Kód patří do handleru. Efektová verze je nepřímá a může se spustit i ve chvíli, kdy to nečekáš (třeba při opakovaném připojení komponenty).
| Situace | ❌ Efekt | ✅ Lepší řešení |
|---|---|---|
| Hodnota spočítaná z props/stavu | useEffect → setTotal(…) | Spočítej při renderu (případně useMemo) |
| Reakce na kliknutí / odeslání | Nastavit příznak a reagovat v efektu | Udělej to rovnou v handleru |
| Reset stavu při změně prop | useEffect(() => setX(init), [id]) | <Form key={id} /> |
| Upozornit rodiče na změnu | useEffect(() => onChange(x), [x]) | Zavolej onChange ve stejném handleru jako setX |
| Řetězení stavů | Efekt nastaví stav, který spustí další efekt… | Spočítej vše v jednom handleru |
| Načtení dat | Holý fetch v efektu | TanStack Query / loader routeru / use() |
Jeden řádek v efektu. Výsledek uvidíš na záložce prohlížeče.
VÝZVA: Počet kliknutí v titulku záložky
1. V efektu nastav document.title = `Kliknuto ${count}×`. Závislost je [count].
2. Úklid už je napsaný: vrátí původní titulek, když komponenta zmizí.
Výsledek uvidíš nahoře na záložce prohlížeče.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).Načítám interaktivní část…
Stopky procvičí kapitoly 2 a 3 (interval, úklid, závislosti, updater). Vyhledávání přidá kapitolu 4: musíš zařídit, aby se zobrazily jen výsledky posledního dotazu. U každého efektu si nejdřív řekni: co rozsvěcím, jak zhasnu a na čem to závisí.
Interval, úklid, updater funkce a dva nezávislé efekty – každý synchronizuje jednu věc (čas a titulek záložky).
Střední a těžké výzvy jsou bonus pro přihlášené. Registrace je zdarma – stačí e-mail, žádné heslo.
Reálný scénář z každé aplikace: našeptávač, který nezahltí server dotazem po každém písmenu a nezobrazí zastaralé výsledky.
Střední a těžké výzvy jsou bonus pro přihlášené. Registrace je zdarma – stačí e-mail, žádné heslo.
ignore nebo AbortController zahodí starou objednávku.