Stav nepatří komponentě, patří místu, kde se vykreslila
Stav se intuitivně chápe jako něco, co má komponenta v sobě. Nemá. Stav drží React a váže si ho na pozici v render tree, tedy ve stromu, který z tvého renderu skutečně vznikl.
Z toho plyne pravidlo, ze kterého vychází všechno ostatní. Když na stejné pozici vykreslíš stejný typ komponenty jako minule, React její stav zachová. Změní se typ, stav zmizí i s celým podstromem. Zmizí komponenta, zmizí stav.
{isFancy ? <Counter isFancy={true} /> : <Counter isFancy={false} />}
Obě větve jsou v JSX jinde, ale ve stromu jsou na stejné pozici a jde o stejný typ. Počítadlo přepnutí přežije, i když se mu vymění všechny props.
React nevidí tvůj kód, vidí výsledek
Tohle je místo, kde intuice selhává nejčastěji. Pozice není místo v souboru, je to místo ve stromu.
if (isFancy) {
return <div><Counter isFancy={true} /><label>...</label></div>;
}
return <div><Counter isFancy={false} /><label>...</label></div>;
Dva samostatné return, dvě různé větve podmínky, a stav se přesto zachová. Obě cesty vyrobí <Counter /> jako první dítě <div>. React tu podmínku nikdy neviděl, dostal jen hotový strom, a v obou případech je na první pozici stejný typ.
Stejným mechanismem funguje mazání. {showB && <Counter />} po přepnutí na false komponentu ze stromu odebere a stav je nenávratně pryč. Když se vrátí, začíná od nuly.
key je identita, ne jen věc pro seznamy
key se učí u polí, ale to je jen jeho nejčastější použití. Ve skutečnosti je to jediný způsob, jak Reactu ručně říct, že dvě věci na stejné pozici nejsou totéž.
<Chat key={to.id} contact={to} />
Bez klíče zůstane rozepsaná zpráva ve formuláři i po přepnutí příjemce, protože <Chat /> je pořád stejný typ na stejné pozici. S klíčem React při změně to.id vidí jinou identitu, starou instanci zahodí a novou postaví od začátku. Celá oprava je jedna prop.
Funguje to i opačně: dvě komponenty rozhozené do různých větví lze klíčem prohlásit za různé, i když se střídají na stejném místě. A klíč stačí unikátní v rámci jednoho rodiče, globálně unikátní být nemusí.
Pravidlo o vnořených komponentách je jen důsledek
Zákaz definovat komponentu uvnitř jiné komponenty se často předává jako stylová konvence. Není. Je to přímý důsledek pravidla o pozici.
export default function MyComponent() {
function MyTextField() {
const [text, setText] = useState('');
return <input value={text} onChange={e => setText(e.target.value)} />;
}
return <MyTextField />;
}
Každý render rodiče vyrobí novou funkci MyTextField. Nová funkce je z pohledu Reactu jiný typ na stejné pozici, takže se stará instance zahodí a stav se resetuje. Prakticky to znamená, že se input maže při každém stisku klávesy. Přesunutí definice na úroveň modulu problém odstraní, protože identita funkce přestane kolísat.
Co si z toho odnést
Když ladíš stav, který se drží, i když se držet nemá, neptej se, jestli je to stejná komponenta. Ptej se, jestli je to stejné místo. Ta otázka odpoví na formulář, který si pamatuje předchozího uživatele, i na modál, který si nese hodnoty z minulého otevření.
Reset stavu není potřeba psát ručně přes useEffect, který při změně props všechno vynuluje. To je obcházení mechanismu, který už existuje. Správný nástroj je key a jeho hodnotou je to, co definuje identitu — ID příjemce, ID záznamu, ID kroku průvodce.
A obráceně: když se stav ztrácí a nemá, hledej něco, co při renderu mění identitu typu. Komponenta definovaná uvnitř jiné je nejčastější případ, ale stejně se chová i typ vytvořený voláním nebo skládáním za běhu.