Lekce 10 · Vzory
Návrhové vzory komponent
Kompozice, compound components, render props, portály a error boundaries.
Načítám lekci…
Lekce 10 · Vzory
Kompozice, compound components, render props, portály a error boundaries.
Načítám lekci…
Hotové autíčko z obchodu je krásné, ale umí jen to, co vymyslel výrobce. Chceš, aby mělo jiná kola? Smůla. Lego je jiný přístup: dostaneš sadu kostek, které do sebe zapadají, a postavíš si, co potřebuješ. Výrobce nemusel předvídat každé přání.
S komponentami je to stejné. Můžeš napsat „hotovou hračku“ – komponentu, která všechno řídí sama přes desítky nastavení. Nebo sadu kostek, které spolu umí spolupracovat a uživatel komponenty si z nich složí, co chce. Návrhové vzory v téhle lekci jsou osvědčené způsoby, jak takové kostky navrhnout.
U každé komponenty si klademe stejnou otázku: kolik kontroly dát tomu, kdo ji používá – a jak mu ji dát, aniž bychom ho zahltili.
Takhle vypadá karta, která začala jednoduše a pak rostla s každým novým požadavkem:
// ❌ „Exploze props“ – každý požadavek = nový prop<Cardtitle="Objednávka"subtitle="č. 1234"icon="cart"showFooterfooterText="Zaplatit"footerAlign="right"onFooterClick={pay}/>
Co když přijde požadavek na dvě tlačítka v patičce? Nebo na odkaz v nadpisu? Přidáš další props a komponenta uvnitř bude plná podmínek. Je to hotová hračka, kterou výrobce pořád předělává.
// ✅ Kompozice – uživatel skládá, co potřebuje<Card><Card.Header icon={<CartIcon />}>Objednávka <small>č. 1234</small></Card.Header><Card.Body>…</Card.Body><Card.Footer><Button variant="ghost">Zpět</Button><Button onClick={pay}>Zaplatit</Button></Card.Footer></Card>
Card teď řeší jen rámeček, odsazení a stíny. Obsah dodá ten, kdo ji používá – a když bude chtít dvě tlačítka, prostě je tam dá. Komponenta se kvůli tomu nemusí měnit.
Orchestr: dirigent stojí uprostřed a hudebníci ho sledují. Každý hudebník ví, kdy hrát, protože vidí dirigenta – ne proto, že by mu někdo poslal lísteček. A ty jako pořadatel jen rozhodneš o sestavě: kolik houslí, kde bude klavír.
Compound components (složené komponenty) fungují stejně. Je to sada komponent, které patří k sobě – jako <select> a <option> v HTML. Hlavní komponenta je dirigent: drží stav a „vysílá“ ho přes skrytý kontext. Ostatní komponenty ho sledují. Uživatel skládá sestavu.
// Takhle to používá uživatel:<Tabs defaultValue="react"><Tabs.List><Tabs.Tab value="react">React</Tabs.Tab><Tabs.Tab value="vue">Vue</Tabs.Tab></Tabs.List><Tabs.Panel value="react">⚛️ Knihovna pro UI</Tabs.Panel><Tabs.Panel value="vue">🟩 Progresivní framework</Tabs.Panel></Tabs>
A takhle je to postavené uvnitř:
const TabsContext = createContext<{ active: string; setActive: (v: string) => void } | null>(null)// Dirigent: drží stav a vysílá hofunction Tabs({ defaultValue, children }) {const [active, setActive] = useState(defaultValue)return <TabsContext value={{ active, setActive }}>{children}</TabsContext>}// Hudebník: sleduje dirigentafunction Tab({ value, children }) {const { active, setActive } = useTabs()return (<button aria-selected={active === value} onClick={() => setActive(value)}>{children}</button>)}function Panel({ value, children }) {const { active } = useTabs()return active === value ? <div>{children}</div> : null}Tabs.Tab = Tab // aby šlo psát <Tabs.Tab>Tabs.Panel = Panel
Co se stane po kliknutí na záložku „Vue“:
Tab s value="vue" zavolá setActive('vue') – funkci, kterou dostal z kontextu.Tabs. Ten vyšle novou hodnotu kontextu.Tab a Panel se překreslí: tlačítko „Vue“ je aktivní, panel React vrátí null, panel Vue se ukáže.Uživatel komponenty nic z toho neřeší. Neposílá active ani onChange do každého tlačítka. A přitom může do sestavy vložit cokoli vlastního – ikonku, odznak, oddělovač.
Co vidíš: Záložky z příkladu výše. U záložky Svelte je vložený vlastní odznak „nové“ – uživatel komponenty si ho tam dal sám, aniž by Tabs musel mít prop na odznaky.
Vyzkoušej:
CompoundTabsDemo dole. Všimni si, že žádný Tab nedostává active ani onClick – všechno si bere z kontextu.useId(). Díky němu jsou ID pro čtečky obrazovky unikátní, i kdyby na stránce bylo víc sad záložek.Co z toho plyne: Dirigent drží stav, hudebníci ho sledují, uživatel skládá sestavu. Logika je na jednom místě, struktura je volná.
Načítám interaktivní část…
Fotoautomat na nádraží zařídí světlo, odpočet, cvaknutí i tisk. Jen pózu dodáš ty. Automat nemusí vědět, jestli se budeš usmívat nebo šklebit.
Render prop je stejný nápad: komponenta řeší logiku (filtrování, stránkování, prázdný stav) a dostane od tebe funkci, která říká, jak má vypadat jedna položka.
function FilterableList<T>({ items, getText, renderItem }: {items: T[]getText: (item: T) => stringrenderItem: (item: T) => ReactNode // ← render prop}) {const [query, setQuery] = useState('')const filtered = items.filter((i) => getText(i).toLowerCase().includes(query.toLowerCase()))return (<><input value={query} onChange={(e) => setQuery(e.target.value)} /><ul>{filtered.map(renderItem)}</ul> {/* vzhled dodá volající */}</>)}// Stejná logika, dvakrát jiný vzhled:<FilterableList items={products} getText={(p) => p.name}renderItem={(p) => <li key={p.id}>{p.name} – <b>{p.price} Kč</b></li>} /><FilterableList items={colors} getText={(c) => c}renderItem={(c) => <li key={c}>🎨 {c}</li>} />
Generický typ T zajistí, že funkce renderItem dostane správně typovanou položku: u produktů Product, u barev string.
Co vidíš: Dva seznamy postavené ze stejné komponenty FilterableList. Vlevo produkty s cenou, vpravo barvy s ikonkou a vlastním textem pro prázdný výsledek.
Vyzkoušej:
Co z toho plyne: Logika je napsaná jednou a vzhled si určí každé použití. Komponenta nemusí znát podobu položek.
Načítám interaktivní část…
Termostat v bytě má dva režimy. V automatickém si teplotu hlídá sám. V ručním ji nastavuješ ty a on jen poslouchá. Je to pořád stejný přístroj – jen se liší, kdo rozhoduje.
Dobrá UI komponenta funguje stejně – přesně jako <input> z lekce o formulářích:
<Switch defaultChecked /> – automat: stav si drží sama (uncontrolled).<Switch checked={wifi} onChange={setWifi} /> – ruční režim: stav drží rodič (controlled).function useControllableState<T>(controlled: T | undefined, defaultValue: T, onChange?: (v: T) => void) {const [internal, setInternal] = useState(defaultValue) // vlastní paměť pro automatconst isControlled = controlled !== undefined // dostali jsme hodnotu zvenku?const value = isControlled ? controlled : internal // kdo rozhodujeconst setValue = (next: T) => {if (!isControlled) setInternal(next) // v automatu si zapiš sámonChange?.(next) // vždy dej vědět rodiči}return [value, setValue] as const}
checked. Když ano, je v ručním režimu a zobrazí hodnotu od rodiče.onChange a čeká, až rodič pošle novou hodnotu.onChange nepředá, nikdo hodnotu nezmění – přepínač je jen pro čtení.Co vidíš: Tři přepínače stejné komponenty Switch: první v automatu, druhý řízený rodičem (stav wifi), třetí řízený bez onChange. Dole tlačítko, kterým rodič mění wifi.
Vyzkoušej:
wifi = … v jeho popisku – hodnotu drží rodič a přepínač ho jen informuje.Co z toho plyne: Jedna komponenta, dva režimy. Uživatel si vybere: nechat ji, ať si poradí sama, nebo ji řídit.
Načítám interaktivní část…
Třída si vyrobí plakát o školním výletě. Kdyby visel jen ve třídě, za skříní, nikdo by ho neviděl. Tak ho připíchnou na nástěnku v hale. Plakát pořád patří té třídě – ona ho vyrobila a stará se o něj –, jen visí jinde.
Modaly, tooltipy a rozbalovací menu mají stejný problém. Když je vykreslíš uvnitř karty, která má overflow: hidden nebo transform, karta je ořízne nebo překryje. Portál je vykreslí jinam v DOM (typicky do <body>), ale v React stromu zůstanou na svém místě:
import { createPortal } from 'react-dom'function Modal({ children, onClose }: { children: ReactNode; onClose: () => void }) {return createPortal(<div className="modal-backdrop" onClick={onClose}><div className="modal" onClick={(e) => e.stopPropagation()}>{children}</div></div>,document.body, // kam to v DOM „připíchnout“)}
<body> – žádná karta ho neořízne.<body>.e.stopPropagation(): bez toho by klik do obsahu „probublal“ k pozadí a modal zavřel.Co vidíš: Karta s overflow: hidden a transform – typická past pro modaly. Dvě tlačítka otevřou stejný modal: jednou vykreslený na místě, jednou přes portál.
Vyzkoušej:
stopPropagation v akci.Co z toho plyne: Portál změní, kde prvek visí v DOM, ale ne, komu patří. Únik z ořezávání bez ztráty kontextu a událostí.
Načítám interaktivní část…
Když v kuchyni zkratuje rychlovarná konvice, vyhodí pojistka – ale jen pro kuchyň. V obýváku dál svítí a televize běží. Kdyby byl celý byt na jedné pojistce, každý zkrat by znamenal tmu všude.
Bez ochrany se React aplikace chová jako byt s jednou pojistkou: chyba při renderu jakékoli komponenty shodí celou stránku – uživatel vidí prázdnou bílou obrazovku. Error boundary je pojistka pro jednu část stromu: chybu zachytí a místo spadlé části ukáže náhradní obsah.
<ErrorBoundary fallback={<p>Graf se nepodařilo zobrazit.</p>}><SalesChart /></ErrorBoundary><ErrorBoundary fallback={<p>Seznam objednávek je nedostupný.</p>}><OrderList /></ErrorBoundary>
Když SalesChart při renderu vyhodí chybu, React vystoupá stromem k nejbližší boundary a vykreslí její fallback. OrderList běží dál. Boundary musí být zatím třída (hook ekvivalent neexistuje); v praxi se používá knihovna react-error-boundary.
Co vidíš: Dva widgety, každý ve vlastní boundary. Oba při čísle 3 schválně spadnou – vyhodí chybu při renderu.
Vyzkoušej:
Co z toho plyne: Každá boundary chrání jen svou část. Chyba v jednom widgetu neshodí druhý ani zbytek stránky.
Načítám interaktivní část…
| Boundary zachytí | Boundary NEzachytí |
|---|---|
| Chyby při renderu potomků | Chyby v event handlerech (použij try/catch) |
| Chyby v efektech potomků | Asynchronní chyby (setTimeout, promise mimo render) |
Odmítnutý Promise v use() | Chybu v boundary samotné |
Jedna karta, různý obsah. Stačí ho vykreslit na správném místě.
VÝZVA: Karta s obsahem
Card dostává nadpis a obsah (children), ale obsah zatím nevykresluje.
1. Do <div> pod nadpisem vykresli {children}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…
Accordion procvičí kapitolu 2: API pro uživatele je hotové, tvým úkolem je postavit dirigenta a hudebníky. Modal spojí kapitolu 5 s refy a efekty – a navíc musí fungovat s klávesnicí i čtečkou obrazovky.
API je hotové – tvým úkolem je rozchodit komponenty „pod kapotou“.
Střední a těžké výzvy jsou bonus pro přihlášené. Registrace je zdarma – stačí e-mail, žádné heslo.
Modal, který funguje s klávesnicí i čtečkou obrazovky – a neoříznou ho rodičovské styly.
Střední a těžké výzvy jsou bonus pro přihlášené. Registrace je zdarma – stačí e-mail, žádné heslo.
defaultX) i controlled (x + onChange).