Lekce 6 · Hooky do hloubky
Context
Konec prop drillingu, vzor Provider + hook a výkonnostní pasti.
Načítám lekci…
Lekce 6 · Hooky do hloubky
Konec prop drillingu, vzor Provider + hook a výkonnostní pasti.
Načítám lekci…
Ředitel školy potřebuje říct žákům ve třetím patře, že odpoledne odpadá tělocvik. Bez rozhlasu by to šlo takhle: ředitel to řekne zástupkyni, ta učiteli na chodbě, ten třídnímu učiteli, a ten konečně žákům. Zástupkyně ani učitel na chodbě tu zprávu nepotřebují – musí ji jen nést dál. A když někdo v řadě zapomene, zpráva se ztratí.
V Reactu se tomu říká prop drilling (provrtávání props). Data tečou jen shora dolů přes props, takže když je potřebuje komponenta hluboko ve stromu, musí je nést každá komponenta mezi nimi – i ta, která je vůbec nepoužívá.
Škola to řeší rozhlasem. Ředitel mluví do mikrofonu v ústředně a slyší ho každý, kdo je v budově a má reproduktor. Nikdo nic nenese. Přesně tohle je Context:
Aplikace má přihlášeného uživatele. Jeho avatar se ukazuje v postranním panelu, který je uvnitř layoutu, který je uvnitř aplikace:
function App() {const [user, setUser] = useState<User>(…)return <Layout user={user} />}function Layout({ user }: { user: User }) { // nepotřebuje, jen nese dálreturn <Sidebar user={user} />}function Sidebar({ user }: { user: User }) { // nepotřebuje, jen nese dálreturn <Avatar user={user} />}function Avatar({ user }: { user: User }) { // konečně ho použijereturn <img src={user.photo} alt={user.name} />}
Proč to vadí:
Layout a Sidebar musí znát typ User a mít prop, který nepotřebují.Dvě nebo tři úrovně jsou v pořádku – props jsou jasné a čitelné. Problém začíná, když se stejná data nesou přes hodně vrstev k hodně místům.
Postavíme rozhlas pro barevné téma (světlé/tmavé), které chce znát spousta komponent.
const ThemeContext = createContext<{ theme: Theme; toggle: () => void } | null>(null)
createContext vytvoří „kanál“. Hodnota v závorce (null) je to, co uslyší komponenta, která je mimo dosah jakéhokoli Provideru – reproduktor bez signálu.
function ThemeProvider({ children }: { children: ReactNode }) {const [theme, setTheme] = useState<Theme>('light')const toggle = () => setTheme((t) => (t === 'light' ? 'dark' : 'light'))return <ThemeContext value={{ theme, toggle }}>{children}</ThemeContext>}// Někde nahoře v aplikaci:<ThemeProvider><Layout /></ThemeProvider>
<ThemeContext value={…}> je v Reactu 19 zápis Provideru. Ve starším kódu uvidíš <ThemeContext.Provider value={…}>.children, a jejich potomci.function useTheme() {const ctx = use(ThemeContext)if (!ctx) throw new Error('useTheme musí být uvnitř <ThemeProvider>')return ctx}// Kdekoli hluboko ve stromu:function ThemedButton() {const { theme, toggle } = useTheme()return <button className={theme} onClick={toggle}>Přepnout</button>}
Proč vlastní hook a ne rovnou use(ThemeContext) v každé komponentě? Když někdo zapomene obalit aplikaci Providerem, bez kontroly by dostal null a o pár řádků dál nesrozumitelnou chybu „cannot read properties of null“. S hookem dostane jasnou zprávu, co je špatně. Navíc komponenty nemusí vědět, jak je kontext uvnitř postavený.
toggle() zavolá setTheme v Provideru. Změnil se stav Provideru.{ theme: "dark", toggle }.Layout mezi nimi se nemusí překreslovat kvůli kontextu – nic z něj nečte.Co vidíš: Malá aplikace: Layout obsahuje Sidebar a panel „Obsah“. Panely a tlačítko čtou téma přes useTheme(). Layout ani Sidebar nedostávají žádný prop o tématu.
Vyzkoušej:
Layout. Neobsahuje ani slovo o tématu – přesto jsou obě její části v souladu.Co z toho plyne: Jedna hodnota ve stavu Provideru, mnoho posluchačů kdekoli pod ním, a nikdo mezi nimi ji nemusí nést.
Načítám interaktivní část…
Rozhlasem se hlásí věci, které zajímají celou školu: konec vyučování, požární poplach. Kdyby se rozhlasem řešilo, kdo komu půjčil gumu, nikdo by se neslyšel. Context se hodí na data, která potřebuje hodně míst a která se mění zřídka.
| ✅ Dobrý kandidát | ❌ Špatný kandidát |
|---|---|
| Téma, jazyk, formát měny | Data, která se mění při každém stisku klávesy |
| Přihlášený uživatel, oprávnění | Serverová data (to je práce pro TanStack Query) |
| Služby: toasty, modaly, analytika | Stav, který stačí předat o 1–2 úrovně props |
| Stav složitého widgetu (Tabs, Accordion) sdílený mezi jeho částmi | Globální „úložiště všeho“ |
Někdy jde o to, že zprávu nesou lidé, kteří ji vůbec nést nemusí. Místo rozhlasu stačí posadit žáka přímo do ředitelny. V Reactu: místo abys data protlačoval přes Layout, předej mu hotový prvek, který už data má:
// ❌ Layout nese user jen proto, aby ho předal dál<Layout user={user} />// ✅ Avatar vytvoříš tam, kde user je. Layout jen určí, KDE se ukáže.<Layout sidebar={<Avatar user={user} />} />function Layout({ sidebar }: { sidebar: ReactNode }) {return <div className="layout"><aside>{sidebar}</aside>…</div>}
Layout teď o uživateli neví nic – a nepotřebuje k tomu ani Context. Tohle je sloty z lekce o základech.
Rozhlas má jednu nevýhodu: když se hlášení změní, poslouchat musí všichni, i když se jich týká jen kousek. Hlásí se „tělocvik odpadá a v jídelně jsou knedlíky“ – a kvůli knedlíkům zpozorní i třída, která tělocvik vůbec nemá.
Context funguje stejně. Když Provider vyšle novou hodnotu (React to pozná porovnáním reference), překreslí se všichni, kdo kontext čtou – i ti, kteří používají jen část hodnoty.
function AppProvider({ children }: { children: ReactNode }) {const [count, setCount] = useState(0)// Při každé změně count vznikne NOVÝ objekt → všichni posluchači se překreslíreturn <AppContext value={{ user: 'Jana', count, inc }}>{children}</AppContext>}function UserBadge() {const { user } = use(AppContext) // čte jen user…return <span>{user}</span> // …ale překreslí se i při každé změně count}
Řešení: místo jednoho kanálu použij víc kanálů – kontext pro každou věc, která se mění zvlášť.
Co vidíš: Dvě stejné dvojice komponent: jméno uživatele a počítadlo. Vlevo čtou obě z jednoho kontextu, vpravo z dvou oddělených. Každá karta žlutě blikne, když se překreslí. Všechny jsou obalené v memo, takže kvůli rodiči se nepřekreslují.
Vyzkoušej:
memo levé kartě se jménem nepomohl. Změna kontextu ho obchází.Co z toho plyne: Každý posluchač se překreslí při každé změně svého kanálu. Co se mění nezávisle, dej do samostatných kontextů.
Načítám interaktivní část…
Častý a velmi účinný vzor je spojit Context s reducerem z minulé lekce. Stav vysílá jeden kanál, funkci dispatch druhý. dispatch se nikdy nemění, takže komponenty, které jen posílají akce (tlačítka „Přidat“), se při změně dat nepřekreslují:
const TasksContext = createContext<Task[] | null>(null)const TasksDispatchContext = createContext<Dispatch<Action> | null>(null)function TasksProvider({ children }: { children: ReactNode }) {const [tasks, dispatch] = useReducer(tasksReducer, [])return (<TasksContext value={tasks}><TasksDispatchContext value={dispatch}>{children}</TasksDispatchContext></TasksContext>)}function AddTaskButton() {const dispatch = useTasksDispatch() // poslouchá jen kanál, který se neměníreturn <button onClick={() => dispatch({ type: 'added', text: 'Nový' })}>Přidat</button>}
Přečti hodnotu z kontextu – bez předávání props přes Middle.
VÝZVA: Barva z kontextu
Kontext s barvou je založený a App ho poskytuje (value="tomato").
1. V komponentě ColoredText si barvu přečti: const color = use(ColorContext)
a použij ji ve stylu: style={{ color }}
ColoredText nedostává žádné props – barvu si vezme sama z kontextu.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…
První výzva procvičí kapitolu 2 – celý vzor Provider + hook. Druhá kapitolu 4: kontext jako „služba“, který vysílá jen funkci, takže komponenty, které notifikace vyvolávají, se nepřekreslují.
Kompletní vzor Provider + hook + obsah podle role. Komponenta Page mezi nimi nesmí o uživateli nic vědět – stejně jako zástupkyně, která už nemusí nést vzkaz.
VÝZVA: Přihlášení přes Context
Komponenty Header, SecretContent a AdminPanel jsou hotové a berou si data z useAuth().
Ty dopíšeš Provider a hook, aby to opravdu fungovalo.
1. AuthProvider: vytvoř stav `user` (User | null, začíná null) a sestav `value`:
login(name) → setUser({ name, role: name === 'admin' ? 'admin' : 'user' })
logout() → setUser(null)
2. AuthProvider: obal `children` do <AuthContext value={value}>.
3. useAuth(): přečti kontext přes use(AuthContext). Když je null (hook je mimo Provider),
vyhoď srozumitelnou chybu: throw new Error('useAuth() musí být uvnitř <AuthProvider>').
Všimni si: Page mezi AuthDemo a Headerem nedostává žádné props o uživateli.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…
Kontext jako „služba“ – přesně tak fungují toasty v knihovnách jako sonner nebo react-hot-toast.
Střední a těžké výzvy jsou bonus pro přihlášené. Registrace je zdarma – stačí e-mail, žádné heslo.
use(Ctx) poslouchá, komponenty mezi nimi o ničem nevědí.createContext(null) + Provider se stavem + hook s kontrolou. V Reactu 19 <Ctx value> a use(Ctx).memo. Rozděl kontexty na víc kanálů a odděl stav od dispatch.