Dva druhy stavu, které si pleteme za jeden
Nejužitečnější věc, kterou tě React Query naučí, není žádné API. Je to jedna hranice, kterou většina aplikací nikdy nenakreslí: stav ze serveru a stav klienta jsou dvě různé věci a chtějí jiné zacházení. Jakmile tu čáru vidíš, spousta „záhadných" bugů s neaktuálními daty se ukáže jako jeden a ten samý problém.
Client state je stav, který vlastníš. Rozbalené menu, rozepsaný formulář, vybraný tab, viditelnost modalu. Vzniká v prohlížeči, žije tam a nikdo jiný ho nemění. Na tohle je useState, useReducer nebo Zustand přesně to pravé.
Server state je stav, který si jen půjčuješ. Seznam uživatelů, detail článku, odpověď z API. Nevlastníš ho — vlastní ho server, ty máš na obrazovce jen kopii k zobrazení. A tady se láme celá architektura.
Půjčená data mají tři vlastnosti, které vlastní nemají
Server state je asynchronní: v čase mezi „chci ho" a „mám ho" existuje stav načítání i stav chyby, se kterými musíš počítat.
Je sdílený: ta samá data vidí a mění i ostatní uživatelé a jiné taby. Tvoje kopie je pravdivá jen v okamžiku, kdy dorazila.
A hlavně zastarává. Jakmile ji máš v ruce, začíná se rozcházet s pravdou na serveru. Client state nikdy nezastará — nikdo jiný ho nemění. Server state zastará vždycky, jen nevíš kdy.
Tyhle tři vlastnosti je přesně to, co obyčejný useState neumí řešit. Proto je zvláštní kategorie nástrojů, které serverovému stavu rozumí.
Proč je zkopírování do useState anti-pattern
Klasický reflex vypadá takhle:
const { data } = useQuery({ queryKey: ['user'], queryFn: fetchUser })
const [user, setUser] = useState(data) // tady se to láme
Vypadá to nevinně, ale právě jsi vyrobil odpojenou kopii. Cache dál žije svým životem — refetchne na pozadí, aktualizuje se po mutaci, sesynchronizuje se mezi taby. Tvoje user ve useState o žádné z těch aktualizací neví. Zmrazil jsi jeden snímek a od té chvíle zobrazuješ minulost.
Pravidlo je proto tvrdé: data ze serveru nekopíruj do lokálního stavu. Nech je v cache a čti je odtud. Když z nich potřebuješ něco odvodit, odvoď to při renderu, ne uložením do dalšího stavu. Jediná legitimní výjimka je předvyplnění formuláře, kde vědomě chceš snímek a řekneš to nahlas přes staleTime: Infinity — od té chvíle to totiž fakticky přestává být server state a stává se to draftem, který vlastníš.
Server jako zdroj pravdy, klient jako cache
Když tuhle hranici přijmeš, mentální model se převrátí. Přestaneš přemýšlet v „stáhnu data a uložím je do stavu" a začneš v „server je zdroj pravdy a já držím jeho cache, kterou průběžně srovnávám".
Dvě čísla ten model řídí. staleTime říká, jak dlouho je čerstvá kopie důvěryhodná — dokud je fresh, čte se jen z cache bez síťové cesty; jakmile zestárne, React Query si dovolí na pozadí refetchnout. gcTime je něco jiného: jak dlouho zůstane neaktivní dotaz v paměti, než ho garbage collector zahodí. První ladíš podle toho, jak rychle tvoje data zastarávají; druhého se skoro nedotkneš.
A query key je kontrakt. Chová se jako pole závislostí — změníš v klíči parametr a React Query automaticky refetchne, protože to už jsou jiná data. Nemusíš orchestrovat „teď zavolej znovu". Popíšeš, co chceš, a synchronizace je deklarativní.
Co si z toho odnést
Než někam uložíš data, zeptej se jedné otázky: vlastním je, nebo si je jen půjčuju ze serveru. Odpověď rozhodne, jaký nástroj sáhne správně. Vlastněný stav do useState. Půjčený stav do cache, která umí async, sdílení a zastarávání — a nikdy ne do obojího zároveň.
Většina „stavu, který se rozešel" a „proč to nevidí novou hodnotu" pramení z jediné chyby: server state se zkopíroval do client state a tím se odřízl od synchronizace. Nakresli tu hranici jednou pořádně a půlka téhle třídy bugů zmizí ještě předtím, než vznikne.