Learning
Všechny články
Frontend14. září 2026 6 min

Suspense nečeká na data, čeká na render

Fallback se zapne jen tehdy, když komponenta suspenduje při renderu

Boundary neposlouchá síť, poslouchá renderovací průchod

<Suspense> se obvykle vysvětlí jako „ukaž spinner, dokud se nenačtou data“. Ta věta vede k první chybě, na kterou každý narazí, protože Suspense o načítání dat neví vůbec nic.

Boundary se aktivuje jedině tak, že komponenta v jejím podstromu suspenduje během renderu. Suspendovat znamená přečíst promise, který ještě není hotový, typicky přes use nebo přes lazy. Nic jiného boundary nespustí.


Proč fetch v efektu fallback nezapne

Tohle je přímý důsledek a stojí za to ho vidět napsaný:

useEffect(() => {
  fetchData(url).then(result => {
    setAlbums(result);
  });
}, [url]);

Obalení téhle komponenty do <Suspense> neudělá nic. Fallback se neobjeví a seznam zůstane prázdný, dokud data nedorazí. Důvod je v pořadí fází: efekt běží až po commitu, tedy v okamžiku, kdy render dávno skončil a nic nesuspendovalo. Totéž platí pro fetch v event handleru.

Dokumentace to říká přímo: Suspense nedetekuje načítání dat uvnitř efektu ani handleru. Aktivuje se jen ve vyjmenovaných případech, mezi které patří lazy, čtení promisu přes use, data streamovaná ze Server Components, stylesheety s precedence a čekání na HTML při streamovaném server renderingu.

Aby use fungovalo bez frameworku, musí být promise cachovaný, aby se napříč rendery používala stejná instance. Framework s podporou Suspense přesně tohle dělá za tebe: drží cache promisů a volá na nich use.


Boundary je jednotka odhalení, ne jednotka načítání

Ve výchozím stavu se všechno uvnitř jedné boundary chová jako jeden celek. Když suspenduje jediná komponenta, fallback nahradí všechny sourozence, a obsah se objeví až naráz, jakmile je hotové všechno.

Postupné odhalování se dělá vnořením:

<Suspense fallback={<BigSpinner />}>
  <Biography />
  <Suspense fallback={<AlbumsGlimmer />}>
    <Albums />
  </Suspense>
</Suspense>

Nejdřív běží velký spinner, po načtení biografie ho vystřídá obsah a na místě alb zůstane jejich vlastní glimmer. Z toho plyne pravidlo, které dokumentace formuluje jako varování: boundaries nemají být jemnější než sekvence načítání, kterou chceš uživateli ukázat. Boundary kolem každé komponenty nezrychlí nic, jen rozdrobí obrazovku.

Sladit to pomáhá i dávkování. React odhaluje připravený obsah nejvýš jednou za 300 ms měřeno od posledního odhalení, takže boundaries hotové v jednom okně naskočí společně místo po jedné.


Transition je ochrana už odhaleného obsahu

Druhá častá past přijde až v provozu. Když už je obsah vidět a komponenta suspenduje znovu, boundary se vrátí k fallbacku a hotová obrazovka zmizí. Při navigaci to vypadá jako výpadek.

Opravou je označit tu aktualizaci za neurgentní:

function navigate(url) {
  startTransition(() => {
    setPage(url);
  });
}

Tím Reactu říkáš, že je lepší nechat viditelnou předchozí stránku než schovat cokoli už odhaleného. Stejnou roli hraje useDeferredValue, kterým se dá držet starý výsledek ztlumený, dokud nedorazí nový.

Má to jednu ostrou hranu. React nezachovává stav renderů, které suspendovaly dřív, než se stihly poprvé připojit. Když se obsah načte, celý suspendovaný podstrom se vykreslí znovu od nuly. A když React už viditelný obsah musí schovat, uklidí layout efekty v tom podstromu a po odhalení je spustí znovu, aby měření DOM neběželo nad schovanou částí.


Co si z toho odnést

Při ladění Suspense se neptej, jestli už dorazila data. Ptej se, jestli něco suspendovalo při renderu. Tahle otázka rovnou vysvětlí prázdnou obrazovku bez spinneru i spinner, který zabral půlku stránky.

Z toho plyne i to, kam boundary umístit. Nekresli je podle struktury komponent, ale podle toho, v jakých krocích má obrazovka naskakovat. Každá boundary je jeden schod v té sekvenci.

A jakmile se obsah jednou ukázal, každou další aktualizaci, která může suspendovat, posílej přes startTransition nebo useDeferredValue. Bez toho si Suspense sám zboří to, co už postavil.

Zdroje

Poslech místo čtení — 2 zdroje k načtení.