Hodnotu si komponenta najde sama, nikdo jí ji neposílá
Context se skoro vždycky vysvětluje jako potrubí. Nahoře nalijeme hodnotu, dole vyteče. Ten obraz je pohodlný a vede k špatným odhadům o výkonu.
Přesnější popis je opačný. Když komponenta při renderu zavolá useContext(SomeContext), React jde od ní nahoru po stromě a hledá nejbližší provider téhle konkrétní identity contextu. Najde ho a vezme jeho hodnotu. Nenajde, vezme výchozí hodnotu z createContext(). Nic neteče dolů, čte se nahoru.
Proč je to stejný mechanismus jako v CSS
Dokumentace to přirovnává k dědičnosti CSS a to přirovnání sedí líp, než se zdá. Vnořený element dědí color z nejbližšího předka, který ho nastavil, dokud ho někdo blíž nepřepíše. Provider vnořený hlouběji přepisuje ten nad sebou přesně takhle.
Z toho plyne první praktický důsledek. Mezilehlé komponenty jsou v tom vztahu neviditelné. Můžeš mezi provider a konzumenta vložit libovolně hluboký strom a nic se nezmění, protože ty komponenty nejsou po cestě, kterou hodnota jde. Jsou jen po cestě, po které se hledá.
Klíčem toho hledání je objekt vrácený z createContext(), ne jméno proměnné. Dva contexty vytvořené dvěma voláními createContext() se stejným výchozím obsahem spolu nemají nic společného. Proto se createContext volá jednou v modulu a importuje se, ne vytváří v komponentě.
Odkud se bere překreslení
Když se změní hodnota providera, překreslí se všechny komponenty, které ten context čtou, bez ohledu na hloubku. Tady se skrývá to, co se v praxi nejčastěji podcení.
Konzument se překresluje podle identity celé hodnoty, ne podle toho, kterou její část si vzal. Tohle je nejčastější příčina problému:
<AppContext value={{ user, theme, setTheme }}>
Ten literál se vyrábí při každém renderu providera znovu. Nová reference se nerovná staré, takže se překreslí každý konzument. Komponenta, která si z hodnoty bere jen theme, se překreslí i tehdy, když se změnil jen user.
Druhá půlka je ještě méně vidět. Předání přes context neprochází přes memo. memo porovnává props, ale hodnota z contextu není prop, je to čtení uvnitř komponenty. Memoizovaný mezičlánek proto překreslení konzumenta nezastaví.
Kdy context nesahat
Dokumentace je v tomhle nezvykle přímá a stojí za to ji brát vážně. Než sáhneš po contextu, zkus dvě věci.
Nejdřív prosté props. Dvanáct props přes dvanáct komponent je otrava, ale je z nich vidět, kdo co používá. Context tuhle stopu maže.
Pak předání JSX jako children. Když protahuješ posts skrz Layout, který je vůbec nepoužívá, chybějící abstrakce není context, ale komponenta. <Layout><Posts posts={posts} /></Layout> ten průchod ruší úplně.
Context je na místě u věcí, které jsou opravdu okolní: motiv, přihlášený účet, aktuální routa. Tedy tam, kde by jinak stejnou hodnotu potřebovaly vzdálené větve stromu.
Co si z toho odnést
Ber context jako rozsah platnosti, ne jako přenos. Provider vymezuje část stromu, ve které daná hodnota platí, a komponenta si ji při čtení najde. Ta představa předpovídá chování líp: proto funguje vnořené přepsání, proto je jedno, co je mezi tím, a proto ho nezastaví memo.
Když ladíš překreslení, dívej se první na identitu hodnoty. Objektový literál v providerovi zruší veškerou memoizaci pod ním. Buď ho memoizuj, nebo rozděl na víc contextů podle toho, co se mění spolu.
A na prop drilling se nejdřív podívej jako na chybějící kompozici. Context, který jen zkracuje předávání, si vezme cenu v podobě neviditelných závislostí a nedá za ni nic.