Hook nemá jméno, má pořadí
Pravidlo hooků zní: volej je jen na nejvyšší úrovni komponenty, nikdy v podmínce, cyklu ani vnořené funkci. Většina lidí ho bere jako otravné omezení lintru. Ve skutečnosti přímo popisuje, jak jsou hooky uvnitř postavené. Když víš proč, přestane to být pravidlo k zapamatování a stane se z toho něco zřejmého.
Klíč je tohle: React nespojuje stav s tvým hookem podle názvu. Nikde si neuloží „tenhle useState je počítadlo". Spojuje ho podle pořadí, ve kterém hook zavoláš.
Spojový seznam pověšený na fiberu
Každá komponenta má svůj fiber — uzel, který drží její stav mezi rendery. Na něm visí pole memoizedState, a to je ve skutečnosti hlava spojového seznamu hooků. První hook je první uzel, druhý hook druhý uzel, a tak dál.
Při renderu si React drží kurzor, který ukazuje na aktuální uzel. Zavoláš useState — React vezme uzel, na který kurzor míří, vrátí z něj hodnotu a kurzor posune na další. Zavoláš useEffect — vezme se další uzel v řadě. Žádné jméno, žádný klíč. Jen „další v pořadí".
Při prvním renderu se seznam staví: každé volání hooku přidá jeden uzel. Při každém dalším renderu se ten samý seznam jen prochází odshora. Proto musí být volání pokaždé stejná a ve stejném pořadí — kurzor spoléhá, že n-té volání hooku vždycky sáhne na n-tý uzel.
Proč podmíněný hook rozbije všechno
Teď je jasné, co se stane, když hook schováš do podmínky:
if (isLoggedIn) {
const [name, setName] = useState('');
}
const [count, setCount] = useState(0);
Při prvním renderu je isLoggedIn true. Seznam má dva uzly: uzel 0 pro name, uzel 1 pro count. Při dalším renderu je podmínka false. useState pro name se nezavolá, takže první volání hooku je teď to pro count — a kurzor mu podstrčí uzel 0, tedy stav, který patřil name. count najednou čte cizí data. Od té chvíle je celý seznam posunutý o jedna.
React tenhle rozjezd neumí uhádnout, protože nemá podle čeho. Jediné, co zaručuje správné napárování, je neměnné pořadí volání. Proto podmínky, cykly a předčasné return před hookem padají — všechny mění, kolik hooků se ten render zavolá.
Co si z toho odnést
Pravidlo hooků není konvence, je to důsledek datové struktury. Hooky jsou spojový seznam na fiberu a čtou se přes kurzor, který se posouvá s každým voláním. Jméno hooku v tom nehraje roli — záleží jen na tom, kolikátý v pořadí je.
Když příště zatoužíš zavolat hook v podmínce, převeď to naopak: podmínku dej dovnitř hooku. Místo if (x) useEffect(...) napiš useEffect(() => { if (x) ... }). Hook se volá vždycky, seznam zůstane stabilní a kurzor sedí. A eslint-plugin-react-hooks ti tohle pohlídá dřív, než se posunutý stav projeví za běhu.