Prozkoumejte React Suspense pro načítání dat nad rámec code splittingu. Pochopte Fetch-As-You-Render, zpracování chyb a nadčasové vzory pro globální aplikace.
Načítání zdrojů s React Suspense: Zvládnutí moderních vzorů pro získávání dat
V dynamickém světě webového vývoje vládne uživatelská zkušenost (UX). Očekává se, že aplikace budou rychlé, responzivní a příjemné na používání, bez ohledu na podmínky sítě nebo schopnosti zařízení. Pro vývojáře v Reactu to často znamená složitou správu stavu, komplexní indikátory načítání a neustálý boj proti tzv. vodopádům při načítání dat (data fetching waterfalls). Vstupuje React Suspense, mocná, i když často nepochopená funkce, navržená tak, aby zásadně změnila způsob, jakým zpracováváme asynchronní operace, zejména načítání dat.
Suspense, původně představený pro code splitting s React.lazy()
, odhaluje svůj skutečný potenciál ve schopnosti organizovat načítání *jakéhokoli* asynchronního zdroje, včetně dat z API. Tento komplexní průvodce se ponoří hluboko do React Suspense pro načítání zdrojů, prozkoumá jeho klíčové koncepty, základní vzory pro načítání dat a praktické úvahy pro budování výkonných a odolných globálních aplikací.
Evoluce načítání dat v Reactu: Od imperativního k deklarativnímu
Po mnoho let se načítání dat v komponentách Reactu primárně spoléhalo na běžný vzor: použití hooku useEffect
k zahájení volání API, správa stavů načítání a chyb pomocí useState
a podmíněné vykreslování na základě těchto stavů. Ačkoli je tento přístup funkční, často vedl k několika problémům:
- Množení stavů načítání: Téměř každá komponenta vyžadující data potřebovala své vlastní stavy
isLoading
,isError
adata
, což vedlo k opakujícímu se kódu (boilerplate). - Vodopády a souběhové stavy (Race Conditions): Vnořené komponenty načítající data často vedly k sekvenčním požadavkům (vodopádům), kdy rodičovská komponenta načetla data, vykreslila se, poté její potomek načetl svá data atd. To zvyšovalo celkovou dobu načítání. Souběhové stavy mohly také nastat, když bylo zahájeno více požadavků a odpovědi dorazily v jiném pořadí.
- Složité zpracování chyb: Distribuce chybových zpráv a logiky pro obnovení napříč mnoha komponentami mohla být těžkopádná a vyžadovala prop drilling nebo řešení pro globální správu stavu.
- Nepříjemná uživatelská zkušenost: Více spinnerů, které se objevují a mizí, nebo náhlé posuny obsahu (layout shifts) mohly vytvářet rušivý zážitek pro uživatele.
- Prop Drilling pro data a stav: Předávání načtených dat a souvisejících stavů načítání/chyb přes více úrovní komponent se stalo běžným zdrojem složitosti.
Zvažte typický scénář načítání dat bez Suspense:
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUser = async () => {
try {
setIsLoading(true);
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setUser(data);
} catch (e) {
setError(e);
} finally {
setIsLoading(false);
}
};
fetchUser();
}, [userId]);
if (isLoading) {
return <p>Načítání profilu uživatele...</p>;
}
if (error) {
return <p style={"color: red;"}>Chyba: {error.message}</p>;
}
if (!user) {
return <p>Nejsou k dispozici žádná data o uživateli.</p>;
}
return (
<div>
<h2>Uživatel: {user.name}</h2>
<p>Email: {user.email}</p>
<!-- Další podrobnosti o uživateli -->
</div>
);
}
function App() {
return (
<div>
<h1>Vítejte v aplikaci</h1>
<UserProfile userId={"123"} />
</div>
);
}
Tento vzor je všudypřítomný, ale nutí komponentu spravovat svůj vlastní asynchronní stav, což často vede k těsnému propojení mezi UI a logikou načítání dat. Suspense nabízí deklarativnější a efektivnější alternativu.
Pochopení React Suspense nad rámec Code Splittingu
Většina vývojářů se poprvé setká se Suspense prostřednictvím React.lazy()
pro code splitting, kde umožňuje odložit načítání kódu komponenty, dokud není potřeba. Například:
import React, { Suspense, lazy } from 'react';
const LazyComponent = lazy(() => import('./MyHeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Načítání komponenty...</div>}>
<LazyComponent />
</Suspense>
);
}
V tomto scénáři, pokud MyHeavyComponent
ještě nebyla načtena, hranice <Suspense>
zachytí promise vyhozený funkcí lazy()
a zobrazí fallback
, dokud není kód komponenty připraven. Klíčovým poznatkem je, že Suspense funguje tak, že zachytává promise vyhozené během vykreslování.
Tento mechanismus není exkluzivní pouze pro načítání kódu. Jakákoli funkce volaná během vykreslování, která vyhodí promise (např. protože zdroj ještě není dostupný), může být zachycena hranicí Suspense výše ve stromu komponent. Když se promise vyřeší, React se pokusí komponentu znovu vykreslit, a pokud je zdroj nyní dostupný, fallback je skryt a zobrazí se skutečný obsah.
Klíčové koncepty Suspense pro načítání dat
Abychom mohli využít Suspense pro načítání dat, musíme pochopit několik klíčových principů:
1. Vyhození Promise
Na rozdíl od tradičního asynchronního kódu, který používá async/await
k řešení promises, Suspense se spoléhá na funkci, která *vyhodí* promise, pokud data nejsou připravena. Když se React pokusí vykreslit komponentu, která takovou funkci volá, a data stále čekají na načtení, je promise vyhozen. React poté 'pozastaví' vykreslování této komponenty a jejích potomků a hledá nejbližší hranici <Suspense>
.
2. Hranice Suspense
Komponenta <Suspense>
funguje jako hranice chyb pro promises. Přijímá prop fallback
, což je UI, které se má vykreslit, zatímco se některý z jejích potomků (nebo jejich potomků) pozastavuje (tj. vyhazuje promise). Jakmile se všechny promises vyhozené v jejím podstromu vyřeší, fallback je nahrazen skutečným obsahem.
Jediná hranice Suspense může spravovat více asynchronních operací. Pokud máte například dvě komponenty v rámci stejné hranice <Suspense>
a každá potřebuje načíst data, fallback se bude zobrazovat, dokud nebudou dokončena *obě* načítání dat. Tím se zabrání zobrazení částečného UI a poskytne se lépe koordinovaný zážitek z načítání.
3. Správce cache/zdrojů (odpovědnost vývojáře)
Je klíčové, že Suspense sám o sobě nezpracovává načítání dat ani cachování. Je to pouze koordinační mechanismus. Aby Suspense fungoval pro načítání dat, potřebujete vrstvu, která:
- Zahájí načítání dat.
- Cachuje výsledek (vyřešená data nebo čekající promise).
- Poskytuje synchronní metodu
read()
, která buď okamžitě vrátí cachovaná data (pokud jsou k dispozici), nebo vyhodí čekající promise (pokud ne).
Tento 'správce zdrojů' je obvykle implementován pomocí jednoduché cache (např. Map nebo objekt) k ukládání stavu každého zdroje (čeká, vyřešeno nebo chyba). I když si to můžete pro demonstrační účely vytvořit ručně, v reálné aplikaci byste použili robustní knihovnu pro načítání dat, která se integruje se Suspense.
4. Concurrent Mode (vylepšení v React 18)
Ačkoli lze Suspense použít i ve starších verzích Reactu, jeho plný výkon se uvolní s Concurrent React (ve výchozím nastavení povolen v React 18 s createRoot
). Concurrent Mode umožňuje Reactu přerušit, pozastavit a obnovit práci na vykreslování. To znamená:
- Neblokující aktualizace UI: Když Suspense zobrazí fallback, React může pokračovat ve vykreslování jiných částí UI, které nejsou pozastaveny, nebo dokonce připravovat nové UI na pozadí, aniž by blokoval hlavní vlákno.
- Přechody (Transitions): Nová API jako
useTransition
vám umožňují označit určité aktualizace jako 'přechody', které React může přerušit a učinit je méně naléhavými, což zajišťuje plynulejší změny UI během načítání dat.
Vzory pro načítání dat se Suspense
Pojďme prozkoumat evoluci vzorů pro načítání dat s příchodem Suspense.
Vzor 1: Fetch-Then-Render (Tradiční přístup s obalením do Suspense)
Toto je klasický přístup, kde jsou data načtena a teprve poté je komponenta vykreslena. I když nevyužíváte mechanismus 'vyhození promise' přímo pro data, můžete obalit komponentu, která *nakonec* vykreslí data, do hranice Suspense, abyste poskytli fallback. Jde spíše o použití Suspense jako obecného orchestrátoru UI pro načítání komponent, které se nakonec stanou připravenými, i když jejich interní načítání dat je stále založeno na tradičním useEffect
.
import React, { Suspense, useState, useEffect } from 'react';
function UserDetails({ userId }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchUserData = async () => {
setIsLoading(true);
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
setUser(data);
setIsLoading(false);
};
fetchUserData();
}, [userId]);
if (isLoading) {
return <p>Načítání detailů uživatele...</p>;
}
return (
<div>
<h3>Uživatel: {user.name}</h3>
<p>Email: {user.email}</p>
</div>
);
}
function App() {
return (
<div>
<h1>Příklad Fetch-Then-Render</h1>
<Suspense fallback={<div>Načítání celé stránky...</div>}>
<UserDetails userId={"1"} />
</Suspense>
</div>
);
}
Výhody: Jednoduché na pochopení, zpětně kompatibilní. Lze použít jako rychlý způsob přidání globálního stavu načítání.
Nevýhody: Neeliminuje boilerplate uvnitř UserDetails
. Stále náchylné k vodopádům, pokud komponenty načítají data sekvenčně. Skutečně nevyužívá mechanismus Suspense 'vyhoď-a-chyť' pro samotná data.
Vzor 2: Render-Then-Fetch (Načítání uvnitř renderu, není pro produkci)
Tento vzor slouží hlavně k ilustraci toho, co s Suspense přímo nedělat, protože může vést k nekonečným smyčkám nebo problémům s výkonem, pokud není pečlivě ošetřen. Zahrnuje pokus o načtení dat nebo volání pozastavující funkce přímo ve fázi vykreslování komponenty, *bez* řádného cachovacího mechanismu.
// NEPOUŽÍVEJTE V PRODUKCI BEZ SPRÁVNÉ VRSTVY PRO CACHOVÁNÍ
// Toto je čistě pro ilustraci, jak by přímé 'vyhození' mohlo koncepčně fungovat.
let fetchedData = null;
let dataPromise = null;
function fetchDataSynchronously(url) {
if (fetchedData) {
return fetchedData;
}
if (!dataPromise) {
dataPromise = fetch(url)
.then(res => res.json())
.then(data => { fetchedData = data; dataPromise = null; return data; })
.catch(err => { dataPromise = null; throw err; });
}
throw dataPromise; // Zde nastupuje Suspense
}
function UserDetailsBadExample({ userId }) {
const user = fetchDataSynchronously(`/api/users/${userId}`);
return (
<div>
<h3>Uživatel: {user.name}</h3>
<p>Email: {user.email}</p>
</div>
);
}
function App() {
return (
<div>
<h1>Render-Then-Fetch (Ilustrativní, NEDOPORUČUJE SE PŘÍMO)</h1>
<Suspense fallback={<div>Načítání uživatele...</div>}>
<UserDetailsBadExample userId={"2"} />
</Suspense>
</div>
);
}
Výhody: Ukazuje, jak může komponenta přímo 'požádat' o data a pozastavit se, pokud nejsou připravena.
Nevýhody: Vysoce problematické pro produkci. Tento manuální, globální systém fetchedData
a dataPromise
je zjednodušený, nezvládá robustně více požadavků, invalidaci nebo chybové stavy. Je to primitivní ilustrace konceptu 'vyhození-promise', nikoli vzor k přijetí.
Vzor 3: Fetch-As-You-Render (Ideální vzor pro Suspense)
Toto je změna paradigmatu, kterou Suspense skutečně umožňuje pro načítání dat. Místo čekání na vykreslení komponenty před načtením jejích dat, nebo načítání všech dat předem, Fetch-As-You-Render znamená, že začnete načítat data *co nejdříve*, často *před* nebo *souběžně s* procesem vykreslování. Komponenty pak 'čtou' data z cache, a pokud data nejsou připravena, pozastaví se. Klíčovou myšlenkou je oddělit logiku načítání dat od logiky vykreslování komponenty.
Pro implementaci Fetch-As-You-Render potřebujete mechanismus k:
- Zahájení načítání dat mimo renderovací funkci komponenty (např. při vstupu na routu nebo kliknutí na tlačítko).
- Uložení promise nebo vyřešených dat do cache.
- Poskytnutí způsobu, jakým mohou komponenty 'číst' z této cache. Pokud data ještě nejsou k dispozici, funkce pro čtení vyhodí čekající promise.
Tento vzor řeší problém vodopádů. Pokud dvě různé komponenty potřebují data, jejich požadavky mohou být zahájeny paralelně a UI se zobrazí až poté, co jsou *obě* připraveny, což je koordinováno jedinou hranicí Suspense.
Manuální implementace (pro pochopení)
Abychom pochopili základní mechaniku, vytvořme si zjednodušený manuální správce zdrojů. V reálné aplikaci byste použili specializovanou knihovnu.
import React, { Suspense } from 'react';
// --- Jednoduchý správce cache/zdrojů --- //
const cache = new Map();
function createResource(promise) {
let status = 'pending';
let result;
let suspender = promise.then(
(r) => {
status = 'success';
result = r;
},
(e) => {
status = 'error';
result = e;
}
);
return {
read() {
if (status === 'pending') {
throw suspender;
} else if (status === 'error') {
throw result;
} else if (status === 'success') {
return result;
}
},
};
}
function fetchData(key, fetcher) {
if (!cache.has(key)) {
cache.set(key, createResource(fetcher()));
}
return cache.get(key);
}
// --- Funkce pro načítání dat --- //
const fetchUserById = (id) => {
console.log(`Načítání uživatele ${id}...`);
return new Promise(resolve => setTimeout(() => {
const users = {
'1': { id: '1', name: 'Alice Smith', email: 'alice@example.com' },
'2': { id: '2', name: 'Bob Johnson', email: 'bob@example.com' },
'3': { id: '3', name: 'Charlie Brown', email: 'charlie@example.com' }
};
resolve(users[id]);
}, 1500));
};
const fetchPostsByUserId = (userId) => {
console.log(`Načítání příspěvků pro uživatele ${userId}...`);
return new Promise(resolve => setTimeout(() => {
const posts = {
'1': [{ id: 'p1', title: 'Můj první příspěvek' }, { id: 'p2', title: 'Cestovatelská dobrodružství' }],
'2': [{ id: 'p3', title: 'Postřehy z kódování' }],
'3': [{ id: 'p4', title: 'Globální trendy' }, { id: 'p5', title: 'Místní kuchyně' }]
};
resolve(posts[userId] || []);
}, 2000));
};
// --- Komponenty --- //
function UserProfile({ userId }) {
const userResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
const user = userResource.read(); // Toto pozastaví vykreslování, pokud data uživatele nejsou připravena
return (
<div>
<h3>Uživatel: {user.name}</h3>
<p>Email: {user.email}</p>
</div>
);
}
function UserPosts({ userId }) {
const postsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
const posts = postsResource.read(); // Toto pozastaví vykreslování, pokud data příspěvků nejsou připravena
return (
<div>
<h4>Příspěvky od {userId}:</h4>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
{posts.length === 0 && <li>Nebyly nalezeny žádné příspěvky.</li>}
</ul>
</div>
);
}
// --- Aplikace --- //
let initialUserResource = null;
let initialPostsResource = null;
function prefetchDataForUser(userId) {
initialUserResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
initialPostsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
}
// Načteme předem některá data ještě před vykreslením komponenty App
prefetchDataForUser('1');
function App() {
return (
<div>
<h1>Fetch-As-You-Render se Suspense</h1>
<p>Toto demonstruje, jak může načítání dat probíhat paralelně, koordinováno Suspense.</p>
<Suspense fallback={<div>Načítání profilu uživatele a příspěvků...</div>}>
<UserProfile userId={"1"} />
<UserPosts userId={"1"} />
</Suspense>
<h2>Další sekce</h2>
<Suspense fallback={<div>Načítání jiného uživatele...</div>}>
<UserProfile userId={"2"} />
</Suspense>
</div>
);
}
V tomto příkladu:
- Funkce
createResource
afetchData
nastavují základní cachovací mechanismus. - Když
UserProfile
neboUserPosts
zavolajíresource.read()
, buď okamžitě získají data, nebo je vyhozen promise. - Nejbližší hranice
<Suspense>
zachytí promise(s) a zobrazí svůj fallback. - Klíčové je, že můžeme zavolat
prefetchDataForUser('1')
*před* vykreslením komponentyApp
, což umožňuje zahájit načítání dat ještě dříve.
Knihovny pro Fetch-As-You-Render
Ruční vytváření a údržba robustního správce zdrojů je složitá. Naštěstí několik vyspělých knihoven pro načítání dat přijalo nebo přijímá Suspense a poskytuje osvědčená řešení:
- React Query (TanStack Query): Nabízí výkonnou vrstvu pro načítání dat a cachování s podporou Suspense. Poskytuje hooky jako
useQuery
, které se mohou pozastavit. Je vynikající pro REST API. - SWR (Stale-While-Revalidate): Další populární a lehká knihovna pro načítání dat, která plně podporuje Suspense. Ideální pro REST API, zaměřuje se na rychlé poskytnutí dat (zastaralých) a jejich následnou revalidaci na pozadí.
- Apollo Client: Komplexní GraphQL klient, který má robustní integraci Suspense pro GraphQL dotazy a mutace.
- Relay: Vlastní GraphQL klient od Facebooku, navržený od základu pro Suspense a Concurrent React. Vyžaduje specifické GraphQL schéma a kompilační krok, ale nabízí bezkonkurenční výkon a konzistenci dat.
- Urql: Lehký a vysoce přizpůsobitelný GraphQL klient s podporou Suspense.
Tyto knihovny abstrahují složitost vytváření a správy zdrojů, zpracovávají cachování, revalidaci, optimistické aktualizace a zpracování chyb, což značně usnadňuje implementaci Fetch-As-You-Render.
Vzor 4: Prefetching s knihovnami podporujícími Suspense
Prefetching je mocná optimalizace, při které proaktivně načítáte data, která bude uživatel pravděpodobně potřebovat v blízké budoucnosti, ještě předtím, než o ně explicitně požádá. To může dramaticky zlepšit vnímaný výkon.
S knihovnami podporujícími Suspense se prefetching stává bezproblémovým. Můžete spouštět načítání dat na základě interakcí uživatele, které okamžitě nemění UI, jako je najetí myší na odkaz nebo tlačítko.
import React, { Suspense } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
// Předpokládejme, že toto jsou vaše volání API
const fetchProductById = async (id) => {
console.log(`Načítání produktu ${id}...`);
return new Promise(resolve => setTimeout(() => {
const products = {
'A001': { id: 'A001', name: 'Globální Widget X', price: 29.99, description: 'Univerzální widget pro mezinárodní použití.' },
'B002': { id: 'B002', name: 'Univerzální Gadget Y', price: 149.99, description: 'Špičkový gadget, oblíbený po celém světě.' },
};
resolve(products[id]);
}, 1000));
};
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true, // Povolit Suspense pro všechny dotazy ve výchozím nastavení
},
},
});
function ProductDetails({ productId }) {
const { data: product } = useQuery({
queryKey: ['product', productId],
queryFn: () => fetchProductById(productId),
});
return (
<div style={{"border": "1px solid #ccc", "padding": "15px", "margin": "10px 0"}}>
<h3>{product.name}</h3>
<p>Cena: ${product.price.toFixed(2)}</p>
<p>{product.description}</p>
</div>
);
}
function ProductList() {
const handleProductHover = (productId) => {
// Načíst data předem, když uživatel najede myší na odkaz produktu
queryClient.prefetchQuery({
queryKey: ['product', productId],
queryFn: () => fetchProductById(productId),
});
console.log(`Přednačítání produktu ${productId}`);
};
return (
<div>
<h2>Dostupné produkty:</h2>
<ul>
<li>
<a href="#" onMouseEnter={() => handleProductHover('A001')}
onClick={(e) => { e.preventDefault(); /* Navigovat nebo zobrazit detaily */ }}
>Globální Widget X (A001)</a>
</li>
<li>
<a href="#" onMouseEnter={() => handleProductHover('B002')}
onClick={(e) => { e.preventDefault(); /* Navigovat nebo zobrazit detaily */ }}
>Univerzální Gadget Y (B002)</a>
</li>
</ul>
<p>Přejeďte myší přes odkaz na produkt, abyste viděli prefetching v akci. Sledujte v záložce sítě.</p>
</div>
);
}
function App() {
const [showProductA, setShowProductA] = React.useState(false);
const [showProductB, setShowProductB] = React.useState(false);
return (
<QueryClientProvider client={queryClient}>
<h1>Prefetching s React Suspense (React Query)</h1>
<ProductList />
<button onClick={() => setShowProductA(true)}>Zobrazit Globální Widget X</button>
<button onClick={() => setShowProductB(true)}>Zobrazit Univerzální Gadget Y</button>
{showProductA && (
<Suspense fallback={<p>Načítání Globálního Widgetu X...</p>}>
<ProductDetails productId="A001" />
</Suspense>
)}
{showProductB && (
<Suspense fallback={<p>Načítání Univerzálního Gadgetu Y...</p>}>
<ProductDetails productId="B002" />
</Suspense>
)}
</QueryClientProvider>
);
}
V tomto příkladu spouští najetí myší na odkaz produktu queryClient.prefetchQuery
, což zahájí načítání dat na pozadí. Pokud uživatel poté klikne na tlačítko pro zobrazení detailů produktu a data jsou již v cache z prefetchingu, komponenta se vykreslí okamžitě bez pozastavení. Pokud prefetching stále probíhá nebo nebyl zahájen, Suspense zobrazí fallback, dokud nebudou data připravena.
Zpracování chyb se Suspense a Error Boundaries
Zatímco Suspense zpracovává stav 'načítání' zobrazením fallbacku, přímo nezpracovává 'chybové' stavy. Pokud se promise vyhozený pozastavující komponentou zamítne (tj. načítání dat selže), tato chyba se bude šířit stromem komponent nahoru. Abyste tyto chyby elegantně zpracovali a zobrazili vhodné UI, musíte použít Error Boundaries (hranice chyb).
Error Boundary je React komponenta, která implementuje buď componentDidCatch
, nebo static getDerivedStateFromError
lifecycle metody. Zachytává JavaScriptové chyby kdekoli ve stromu svých potomků, včetně chyb vyhozených promises, které by Suspense normálně zachytil, kdyby čekaly na vyřešení.
import React, { Suspense, useState } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
// --- Komponenta pro hranici chyb --- //
class MyErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
// Aktualizovat stav, aby další vykreslení zobrazilo záložní UI.
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Chybu můžete také zaznamenat do služby pro hlášení chyb
console.error("Byla zachycena chyba:", error, errorInfo);
}
render() {
if (this.state.hasError) {
// Můžete vykreslit jakékoli vlastní záložní UI
return (
<div style={{"border": "2px solid red", "padding": "20px", "margin": "20px 0", "background": "#ffe0e0"}}>
<h2>Něco se pokazilo!</h2>
<p>{this.state.error && this.state.error.message}</p>
<p>Zkuste prosím obnovit stránku nebo kontaktujte podporu.</p>
<button onClick={() => this.setState({ hasError: false, error: null })}>Zkusit znovu</button>
</div>
);
}
return this.props.children;
}
}
// --- Načítání dat (s možností chyby) --- //
const fetchItemById = async (id) => {
console.log(`Pokus o načtení položky ${id}...`);
return new Promise((resolve, reject) => setTimeout(() => {
if (id === 'error-item') {
reject(new Error('Nepodařilo se načíst položku: Síť nedostupná nebo položka nenalezena.'));
} else if (id === 'slow-item') {
resolve({ id: 'slow-item', name: 'Doručeno pomalu', data: 'Tato položka trvala déle, ale dorazila!', status: 'success' });
} else {
resolve({ id, name: `Položka ${id}`, data: `Data pro položku ${id}` });
}
}, id === 'slow-item' ? 3000 : 800));
};
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true,
retry: false, // Pro demonstraci zakážeme opakování, aby byla chyba okamžitá
},
},
});
function DisplayItem({ itemId }) {
const { data: item } = useQuery({
queryKey: ['item', itemId],
queryFn: () => fetchItemById(itemId),
});
return (
<div>
<h3>Detaily položky:</h3>
<p>ID: {item.id}</p>
<p>Název: {item.name}</p>
<p>Data: {item.data}</p>
</div>
);
}
function App() {
const [fetchType, setFetchType] = useState('normal-item');
return (
<QueryClientProvider client={queryClient}>
<h1>Suspense a Error Boundaries</h1>
<div>
<button onClick={() => setFetchType('normal-item')}>Načíst normální položku</button>
<button onClick={() => setFetchType('slow-item')}>Načíst pomalou položku</button>
<button onClick={() => setFetchType('error-item')}>Načíst chybnou položku</button>
</div>
<MyErrorBoundary>
<Suspense fallback={<p>Načítání položky přes Suspense...</p>}>
<DisplayItem itemId={fetchType} />
</Suspense>
</MyErrorBoundary>
</QueryClientProvider>
);
}
Obalením vaší hranice Suspense (nebo komponent, které se mohou pozastavit) do Error Boundary zajistíte, že selhání sítě nebo chyby serveru během načítání dat budou zachyceny a elegantně zpracovány, čímž se zabrání pádu celé aplikace. To poskytuje robustní a uživatelsky přívětivý zážitek, který uživatelům umožňuje pochopit problém a případně to zkusit znovu.
Správa stavu a invalidace dat se Suspense
Je důležité si ujasnit, že React Suspense primárně řeší počáteční stav načítání asynchronních zdrojů. Vnitřně nespravuje cache na straně klienta, nezpracovává invalidaci dat ani neorganizuje mutace (operace create, update, delete) a jejich následné aktualizace UI.
Zde se stávají nepostradatelnými knihovny pro načítání dat podporující Suspense (React Query, SWR, Apollo Client, Relay). Doplňují Suspense tím, že poskytují:
- Robustní cachování: Udržují sofistikovanou in-memory cache načtených dat, kterou okamžitě servírují, pokud je k dispozici, a zpracovávají revalidaci na pozadí.
- Invalidace a znovunačtení dat: Nabízejí mechanismy pro označení cachovaných dat jako 'zastaralých' a jejich znovunačtení (např. po mutaci, interakci uživatele nebo při zaměření okna).
- Optimistické aktualizace: Pro mutace vám umožňují okamžitě (optimisticky) aktualizovat UI na základě očekávaného výsledku volání API a poté se vrátit zpět, pokud skutečné volání API selže.
- Globální synchronizace stavu: Zajišťují, že pokud se data změní v jedné části vaší aplikace, všechny komponenty zobrazující tato data se automaticky aktualizují.
- Stavy načítání a chyb pro mutace: Zatímco
useQuery
se může pozastavit,useMutation
obvykle poskytuje stavyisLoading
aisError
pro samotný proces mutace, protože mutace jsou často interaktivní a vyžadují okamžitou zpětnou vazbu.
Bez robustní knihovny pro načítání dat by implementace těchto funkcí nad manuálním správcem zdrojů pro Suspense byla významným úkolem, v podstatě byste si museli vytvořit vlastní framework pro načítání dat.
Praktické úvahy a osvědčené postupy
Přijetí Suspense pro načítání dat je významným architektonickým rozhodnutím. Zde jsou některé praktické úvahy pro globální aplikaci:
1. Ne všechna data potřebují Suspense
Suspense je ideální pro kritická data, která přímo ovlivňují počáteční vykreslení komponenty. Pro nekritická data, načítání na pozadí nebo data, která lze načíst líně bez silného vizuálního dopadu, může být stále vhodný tradiční useEffect
nebo před-renderování. Nadměrné používání Suspense může vést k méně granulárnímu zážitku z načítání, protože jediná hranice Suspense čeká, až se vyřeší *všichni* její potomci.
2. Granularita hranic Suspense
Promyšleně umisťujte své hranice <Suspense>
. Jediná velká hranice na vrcholu vaší aplikace může skrýt celou stránku za spinner, což může být frustrující. Menší, granulárnější hranice umožňují různým částem vaší stránky načítat se nezávisle, což poskytuje progresivnější a responzivnější zážitek. Například hranice kolem komponenty uživatelského profilu a další kolem seznamu doporučených produktů.
<div>
<h1>Stránka produktu</h1>
<Suspense fallback={<p>Načítání hlavních detailů produktu...</p>}>
<ProductDetails id="prod123" />
</Suspense>
<hr />
<h2>Související produkty</h2>
<Suspense fallback={<p>Načítání souvisejících produktů...</p>}>
<RelatedProducts category="electronics" />
</Suspense>
</div>
Tento přístup znamená, že uživatelé mohou vidět hlavní detaily produktu, i když se související produkty stále načítají.
3. Server-Side Rendering (SSR) a streamování HTML
Nová streamovací SSR API v React 18 (renderToPipeableStream
) se plně integrují se Suspense. To umožňuje vašemu serveru odeslat HTML, jakmile je připraveno, i když se části stránky (jako komponenty závislé na datech) stále načítají. Server může streamovat zástupný symbol (z fallbacku Suspense) a poté streamovat skutečný obsah, když se data vyřeší, bez nutnosti úplného znovuvykreslení na straně klienta. To výrazně zlepšuje vnímaný výkon načítání pro globální uživatele na různých síťových podmínkách.
4. Inkrementální přijetí
Nemusíte přepisovat celou svou aplikaci, abyste mohli používat Suspense. Můžete ho zavádět postupně, počínaje novými funkcemi nebo komponentami, které by nejvíce těžily z jeho deklarativních vzorů pro načítání.
5. Nástroje a ladění
Zatímco Suspense zjednodušuje logiku komponent, ladění může být jiné. React DevTools poskytují vhled do hranic Suspense a jejich stavů. Seznamte se s tím, jak vámi zvolená knihovna pro načítání dat odhaluje svůj vnitřní stav (např. React Query Devtools).
6. Časové limity pro fallbacky Suspense
Pro velmi dlouhé doby načítání můžete chtít zavést časový limit pro váš fallback Suspense, nebo po určitém zpoždění přepnout na podrobnější indikátor načítání. Hooky useDeferredValue
a useTransition
v React 18 mohou pomoci spravovat tyto jemnější stavy načítání, což vám umožní zobrazit 'starou' verzi UI, zatímco se načítají nová data, nebo odložit neurgentní aktualizace.
Budoucnost načítání dat v Reactu: React Server Components a dále
Cesta načítání dat v Reactu nekončí u Suspense na straně klienta. React Server Components (RSC) představují významnou evoluci, slibují stírat hranice mezi klientem a serverem a dále optimalizovat načítání dat.
- React Server Components (RSC): Tyto komponenty se vykreslují na serveru, načítají svá data přímo a poté posílají do prohlížeče pouze nezbytné HTML a JavaScript na straně klienta. To eliminuje vodopády na straně klienta, zmenšuje velikost balíčků a zlepšuje počáteční výkon načítání. RSC pracují ruku v ruce se Suspense: serverové komponenty se mohou pozastavit, pokud jejich data nejsou připravena, a server může klientovi streamovat fallback Suspense, který je poté nahrazen, když se data vyřeší. To je zásadní změna pro aplikace se složitými datovými požadavky, která nabízí bezproblémový a vysoce výkonný zážitek, což je obzvláště výhodné pro uživatele v různých geografických oblastech s různou latencí.
- Jednotné načítání dat: Dlouhodobou vizí pro React je jednotný přístup k načítání dat, kde jádro frameworku nebo úzce integrovaná řešení poskytují prvotřídní podporu pro načítání dat jak na serveru, tak na klientu, vše koordinováno Suspense.
- Pokračující evoluce knihoven: Knihovny pro načítání dat se budou nadále vyvíjet a nabízet ještě sofistikovanější funkce pro cachování, invalidaci a aktualizace v reálném čase, stavějící na základních schopnostech Suspense.
Jak React dále dospívá, Suspense bude stále ústřednějším dílkem skládačky pro budování vysoce výkonných, uživatelsky přívětivých a udržitelných aplikací. Tlačí vývojáře k deklarativnějšímu a odolnějšímu způsobu zpracování asynchronních operací, přesouvá složitost z jednotlivých komponent do dobře spravované datové vrstvy.
Závěr
React Suspense, původně funkce pro code splitting, se rozvinul v transformační nástroj pro načítání dat. Přijetím vzoru Fetch-As-You-Render a využitím knihoven podporujících Suspense mohou vývojáři výrazně zlepšit uživatelskou zkušenost svých aplikací, eliminovat vodopády při načítání, zjednodušit logiku komponent a poskytnout plynulé, koordinované stavy načítání. V kombinaci s Error Boundaries pro robustní zpracování chyb a budoucím příslibem React Server Components nám Suspense umožňuje vytvářet aplikace, které jsou nejen výkonné a odolné, ale také přirozeně příjemnější pro uživatele po celém světě. Přechod na paradigma načítání dat řízené Suspense vyžaduje koncepční změnu, ale přínosy v podobě přehlednosti kódu, výkonu a spokojenosti uživatelů jsou značné a stojí za investici.