Preskúmajte React Suspense na načítavanie dát mimo delenia kódu. Pochopte Fetch-As-You-Render, spracovanie chýb a nadčasové vzory pre globálne aplikácie.
Načítavanie zdrojov s React Suspense: Zvládnutie moderných vzorov načítavania dát
V dynamickom svete webového vývoja je používateľský zážitok (UX) prvoradý. Očakáva sa, že aplikácie budú rýchle, responzívne a príjemné na používanie, bez ohľadu na podmienky siete alebo schopnosti zariadenia. Pre vývojárov v Reacte to často znamená zložitú správu stavov, komplexné indikátory načítavania a neustály boj proti vodopádom pri načítavaní dát. Prichádza React Suspense, silná, hoci často nepochopená, funkcia navrhnutá tak, aby zásadne zmenila spôsob, akým narábame s asynchrónnymi operáciami, najmä s načítavaním dát.
Pôvodne predstavený pre delenie kódu s React.lazy()
, skutočný potenciál Suspense spočíva v jeho schopnosti organizovať načítavanie *akéhokoľvek* asynchrónneho zdroja, vrátane dát z API. Tento komplexný sprievodca sa ponorí hlboko do React Suspense pre načítavanie zdrojov, preskúma jeho základné koncepty, fundamentálne vzory načítavania dát a praktické úvahy pre budovanie výkonných a odolných globálnych aplikácií.
Evolúcia načítavania dát v Reacte: Od imperatívneho k deklaratívnemu
Po mnoho rokov sa načítavanie dát v komponentoch Reactu primárne spoliehalo na bežný vzor: použitie hooku useEffect
na spustenie volania API, správa stavov načítavania a chýb pomocou useState
a podmienené renderovanie na základe týchto stavov. Hoci tento prístup bol funkčný, často viedol k niekoľkým problémom:
- Rozširovanie stavov načítavania: Takmer každý komponent vyžadujúci dáta potreboval vlastné stavy
isLoading
,isError
adata
, čo viedlo k opakujúcemu sa boilerplate kódu. - Vodopády a súbehové stavy (Race Conditions): Vnorené komponenty načítavajúce dáta často viedli k sekvenčným požiadavkám (vodopádom), kde rodičovský komponent načítal dáta, potom sa renderoval, potom detský komponent načítal svoje dáta, a tak ďalej. To zvyšovalo celkové časy načítavania. Súbehové stavy mohli tiež nastať, keď bolo iniciovaných viacero požiadaviek a odpovede prichádzali v nesprávnom poradí.
- Komplexné spracovanie chýb: Distribúcia chybových správ a logiky obnovy naprieč mnohými komponentmi mohla byť ťažkopádna, vyžadujúc si preposielanie props (prop drilling) alebo globálne riešenia na správu stavu.
- Nepríjemný používateľský zážitok: Viacero spinnerov, ktoré sa objavujú a miznú, alebo náhle posuny obsahu (layout shifts), mohli vytvoriť rušivý zážitok pre používateľov.
- Prop Drilling pre dáta a stav: Preposielanie načítaných dát a súvisiacich stavov načítavania/chyby cez viacero úrovní komponentov sa stalo bežným zdrojom zložitosti.
Zvážte typický scenár načítavania dát 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čítava sa profil používateľa...</p>;
}
if (error) {
return <p style={"color: red;"}>Chyba: {error.message}</p>;
}
if (!user) {
return <p>Nie sú dostupné žiadne údaje o používateľovi.</p>;
}
return (
<div>
<h2>Používateľ: {user.name}</h2>
<p>Email: {user.email}</p>
<!-- Ďalšie detaily o používateľovi -->
</div>
);
}
function App() {
return (
<div>
<h1>Vitajte v aplikácii</h1>
<UserProfile userId={"123"} />
</div>
);
}
Tento vzor je všadeprítomný, ale núti komponent spravovať svoj vlastný asynchrónny stav, čo často vedie k tesne prepojenému vzťahu medzi UI a logikou načítavania dát. Suspense ponúka deklaratívnejšiu a zjednodušenú alternatívu.
Pochopenie React Suspense mimo delenia kódu
Väčšina vývojárov sa so Suspense prvýkrát stretne prostredníctvom React.lazy()
pre delenie kódu, kde umožňuje odložiť načítanie kódu komponentu, kým nie je potrebný. Napríklad:
import React, { Suspense, lazy } from 'react';
const LazyComponent = lazy(() => import('./MyHeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Načítava sa komponent...</div>}>
<LazyComponent />
</Suspense>
);
}
V tomto scenári, ak MyHeavyComponent
ešte nebol načítaný, hranica <Suspense>
zachytí promise (sľub) vyvolaný funkciou lazy()
a zobrazí fallback
, kým kód komponentu nebude pripravený. Kľúčovým poznatkom je, že Suspense funguje tak, že zachytáva promises (sľuby) vyvolané počas renderovania.
Tento mechanizmus nie je exkluzívny len pre načítavanie kódu. Akákoľvek funkcia volaná počas renderovania, ktorá vyvolá promise (napr. pretože zdroj ešte nie je dostupný), môže byť zachytená hranicou Suspense vyššie v strome komponentov. Keď sa promise vyrieši, React sa pokúsi znova renderovať komponent, a ak je zdroj teraz dostupný, fallback sa skryje a zobrazí sa skutočný obsah.
Základné koncepty Suspense pre načítavanie dát
Aby sme mohli využiť Suspense na načítavanie dát, musíme porozumieť niekoľkým základným princípom:
1. Vyvolanie Promise (sľubu)
Na rozdiel od tradičného asynchrónneho kódu, ktorý používa async/await
na riešenie promises, Suspense sa spolieha na funkciu, ktorá *vyvolá* promise, ak dáta nie sú pripravené. Keď sa React pokúsi renderovať komponent, ktorý volá takúto funkciu, a dáta sú stále čakajúce, promise je vyvolaný. React potom 'pozastaví' renderovanie tohto komponentu a jeho detí, hľadajúc najbližšiu hranicu <Suspense>
.
2. Hranica Suspense
Komponent <Suspense>
funguje ako hranica chyby pre promises. Prijíma prop fallback
, čo je UI, ktoré sa má renderovať, zatiaľ čo niektoré z jeho detí (alebo ich potomkov) sú v stave pozastavenia (t.j. vyvolávajú promise). Akonáhle sa všetky promises vyvolané v jeho podstrome vyriešia, fallback je nahradený skutočným obsahom.
Jedna hranica Suspense môže spravovať viacero asynchrónnych operácií. Napríklad, ak máte dva komponenty v rámci tej istej hranice <Suspense>
a každý potrebuje načítať dáta, fallback sa zobrazí, kým nebudú dokončené *obidva* procesy načítania dát. Tým sa zabráni zobrazovaniu čiastočného UI a poskytne sa koordinovanejší zážitok z načítavania.
3. Správca keše/zdrojov (zodpovednosť vývojára)
Je dôležité si uvedomiť, že Suspense sám o sebe nespravuje načítavanie dát ani kešovanie. Je to len koordinačný mechanizmus. Aby Suspense fungoval na načítavanie dát, potrebujete vrstvu, ktorá:
- Iniciuje načítanie dát.
- Kešuje výsledok (vyriešené dáta alebo čakajúci promise).
- Poskytuje synchrónnu metódu
read()
, ktorá buď okamžite vráti kešované dáta (ak sú dostupné), alebo vyvolá čakajúci promise (ak nie sú).
Tento 'správca zdrojov' je typicky implementovaný pomocou jednoduchej keše (napr. Map alebo objekt) na ukladanie stavu každého zdroja (čakajúci, vyriešený alebo chybný). Hoci si to môžete vytvoriť manuálne na demonštračné účely, v reálnej aplikácii by ste použili robustnú knižnicu na načítavanie dát, ktorá sa integruje so Suspense.
4. Concurrent Mode (vylepšenia v React 18)
Hoci Suspense sa dá použiť aj v starších verziách Reactu, jeho plný potenciál sa uvoľní s Concurrent Reactom (štandardne povolený v React 18 s createRoot
). Concurrent Mode umožňuje Reactu prerušiť, pozastaviť a obnoviť prácu na renderovaní. To znamená:
- Neblokujúce aktualizácie UI: Keď Suspense zobrazí fallback, React môže pokračovať v renderovaní iných častí UI, ktoré nie sú pozastavené, alebo dokonca pripravovať nové UI na pozadí bez blokovania hlavného vlákna.
- Prechody (Transitions): Nové API ako
useTransition
vám umožňujú označiť určité aktualizácie ako 'prechody', ktoré React môže prerušiť a urobiť menej urgentnými, čím poskytuje plynulejšie zmeny UI počas načítavania dát.
Vzory načítavania dát so Suspense
Preskúmajme evolúciu vzorov načítavania dát s príchodom Suspense.
Vzor 1: Fetch-Then-Render (tradičný prístup s obalením v Suspense)
Toto je klasický prístup, kde sa dáta načítajú a až potom sa komponent renderuje. Hoci priamo nevyužíva mechanizmus 'vyvolania promise' pre dáta, môžete obaliť komponent, ktorý *nakoniec* renderuje dáta, do hranice Suspense, aby ste poskytli fallback. Ide skôr o použitie Suspense ako generického orchestrátora UI načítavania pre komponenty, ktoré sa nakoniec stanú pripravenými, aj keď ich interné načítavanie dát je stále založené na tradičnom 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čítavajú sa detaily používateľa...</p>;
}
return (
<div>
<h3>Používateľ: {user.name}</h3>
<p>Email: {user.email}</p>
</div>
);
}
function App() {
return (
<div>
<h1>Príklad Fetch-Then-Render</h1>
<Suspense fallback={<div>Celkové načítavanie stránky...</div>}>
<UserDetails userId={"1"} />
</Suspense>
</div>
);
}
Výhody: Jednoduché na pochopenie, spätne kompatibilné. Môže sa použiť ako rýchly spôsob pridania globálneho stavu načítavania.
Nevýhody: Neeliminuje boilerplate kód vnútri UserDetails
. Stále náchylné na vodopády, ak komponenty načítavajú dáta sekvenčne. Skutočne nevyužíva mechanizmus 'vyvolaj-a-chyť' Suspense pre samotné dáta.
Vzor 2: Render-Then-Fetch (načítavanie vnútri renderovania, nie pre produkciu)
Tento vzor slúži primárne na ilustráciu toho, čo nerobiť so Suspense priamo, pretože to môže viesť k nekonečným cyklom alebo problémom s výkonom, ak sa to nerieši dôkladne. Zahŕňa pokus o načítanie dát alebo volanie pozastavujúcej funkcie priamo vo fáze renderovania komponentu, *bez* správneho mechanizmu kešovania.
// NEPOUŽÍVAJTE V PRODUKCII BEZ SPRÁVNEJ VRSTVY KEŠOVANIA
// Toto je čisto na ilustráciu toho, ako by priame 'vyvolanie' mohlo koncepčne fungovať.
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; // Tu nastupuje Suspense
}
function UserDetailsBadExample({ userId }) {
const user = fetchDataSynchronously(`/api/users/${userId}`);
return (
<div>
<h3>Používateľ: {user.name}</h3>
<p>Email: {user.email}</p>
</div>
);
}
function App() {
return (
<div>
<h1>Render-Then-Fetch (ilustračné, NEODPORÚČA SA priamo)</h1>
<Suspense fallback={<div>Načítava sa používateľ...</div>}>
<UserDetailsBadExample userId={"2"} />
</Suspense>
</div>
);
}
Výhody: Ukazuje, ako môže komponent priamo 'požiadať' o dáta a pozastaviť sa, ak nie sú pripravené.
Nevýhody: Veľmi problematické pre produkciu. Tento manuálny, globálny systém fetchedData
a dataPromise
je zjednodušený, nerieši robustne viacero požiadaviek, invalidáciu ani chybové stavy. Je to primitívna ilustrácia konceptu 'vyvolania promise', nie vzor, ktorý by sa mal osvojiť.
Vzor 3: Fetch-As-You-Render (ideálny vzor pre Suspense)
Toto je zmena paradigmy, ktorú Suspense skutočne umožňuje pre načítavanie dát. Namiesto čakania na renderovanie komponentu pred načítaním jeho dát, alebo načítania všetkých dát vopred, Fetch-As-You-Render znamená, že začnete načítavať dáta *čo najskôr*, často *pred* alebo *súbežne s* procesom renderovania. Komponenty potom 'čítajú' dáta z keše, a ak dáta nie sú pripravené, pozastavia sa. Základnou myšlienkou je oddeliť logiku načítavania dát od logiky renderovania komponentu.
Na implementáciu Fetch-As-You-Render potrebujete mechanizmus na:
- Iniciovanie načítania dát mimo renderovacej funkcie komponentu (napr. pri vstupe na routu alebo po kliknutí na tlačidlo).
- Uloženie promise alebo vyriešených dát do keše.
- Poskytnutie spôsobu, akým môžu komponenty 'čítať' z tejto keše. Ak dáta ešte nie sú dostupné, funkcia čítania vyvolá čakajúci promise.
Tento vzor rieši problém vodopádu. Ak dva rôzne komponenty potrebujú dáta, ich požiadavky môžu byť iniciované paralelne a UI sa zobrazí až vtedy, keď budú *obidva* pripravené, čo je koordinované jednou hranicou Suspense.
Manuálna implementácia (pre pochopenie)
Aby sme pochopili základné mechanizmy, vytvorme si zjednodušeného manuálneho správcu zdrojov. V reálnej aplikácii by ste použili dedikovanú knižnicu.
import React, { Suspense } from 'react';
// --- Jednoduchý správca keše/zdrojov --- //
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);
}
// --- Funkcie na načítavanie dát --- //
const fetchUserById = (id) => {
console.log(`Načítava sa používateľ ${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čítavajú sa príspevky pre používateľa ${userId}...`);
return new Promise(resolve => setTimeout(() => {
const posts = {
'1': [{ id: 'p1', title: 'Môj prvý príspevok' }, { id: 'p2', title: 'Cestovateľské dobrodružstvá' }],
'2': [{ id: 'p3', title: 'Postrehy z kódovania' }],
'3': [{ id: 'p4', title: 'Globálne trendy' }, { id: 'p5', title: 'Miestna kuchyňa' }]
};
resolve(posts[userId] || []);
}, 2000));
};
// --- Komponenty --- //
function UserProfile({ userId }) {
const userResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
const user = userResource.read(); // Toto spôsobí suspendovanie, ak údaje používateľa nie sú pripravené
return (
<div>
<h3>Používateľ: {user.name}</h3>
<p>Email: {user.email}</p>
</div>
);
}
function UserPosts({ userId }) {
const postsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
const posts = postsResource.read(); // Toto spôsobí suspendovanie, ak údaje príspevkov nie sú pripravené
return (
<div>
<h4>Príspevky od {userId}:</h4>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
{posts.length === 0 && <li>Nenašli sa žiadne príspevky.</li>}
</ul>
</div>
);
}
// --- Aplikácia --- //
let initialUserResource = null;
let initialPostsResource = null;
function prefetchDataForUser(userId) {
initialUserResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
initialPostsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
}
// Prednačítanie niektorých dát ešte pred renderovaním komponentu App
prefetchDataForUser('1');
function App() {
return (
<div>
<h1>Fetch-As-You-Render so Suspense</h1>
<p>Toto demonštruje, ako môže načítavanie dát prebiehať paralelne, koordinované pomocou Suspense.</p>
<Suspense fallback={<div>Načítava sa profil používateľa a príspevky...</div>}>
<UserProfile userId={"1"} />
<UserPosts userId={"1"} />
</Suspense>
<h2>Ďalšia sekcia</h2>
<Suspense fallback={<div>Načítava sa iný používateľ...</div>}>
<UserProfile userId={"2"} />
</Suspense>
</div>
);
}
V tomto príklade:
- Funkcie
createResource
afetchData
vytvárajú základný mechanizmus kešovania. - Keď
UserProfile
aleboUserPosts
volajúresource.read()
, buď okamžite dostanú dáta, alebo je vyvolaný promise. - Najbližšia hranica
<Suspense>
zachytí promise(s) a zobrazí svoj fallback. - Kľúčové je, že môžeme volať
prefetchDataForUser('1')
*pred* renderovaním komponentuApp
, čo umožňuje začať načítavanie dát ešte skôr.
Knižnice pre Fetch-As-You-Render
Vytváranie a údržba robustného správcu zdrojov je zložité. Našťastie, niekoľko zrelých knižníc na načítavanie dát prijalo alebo prijíma Suspense, poskytujúc osvedčené riešenia:
- React Query (TanStack Query): Ponúka silnú vrstvu na načítavanie a kešovanie dát s podporou Suspense. Poskytuje hooky ako
useQuery
, ktoré môžu pozastaviť. Je vynikajúca pre REST API. - SWR (Stale-While-Revalidate): Ďalšia populárna a ľahká knižnica na načítavanie dát, ktorá plne podporuje Suspense. Ideálna pre REST API, zameriava sa na rýchle poskytnutie dát (zastaralých) a ich následnú revalidáciu na pozadí.
- Apollo Client: Komplexný GraphQL klient, ktorý má robustnú integráciu so Suspense pre GraphQL queries a mutations.
- Relay: Vlastný GraphQL klient od Facebooku, navrhnutý od základov pre Suspense a Concurrent React. Vyžaduje špecifickú GraphQL schému a krok kompilácie, ale ponúka neprekonateľný výkon a konzistenciu dát.
- Urql: Ľahký a vysoko prispôsobiteľný GraphQL klient s podporou Suspense.
Tieto knižnice abstrahujú zložitosť vytvárania a správy zdrojov, riešia kešovanie, revalidáciu, optimistické aktualizácie a spracovanie chýb, čo značne zjednodušuje implementáciu Fetch-As-You-Render.
Vzor 4: Prednačítavanie s knižnicami podporujúcimi Suspense
Prednačítavanie je silná optimalizácia, pri ktorej proaktívne načítavate dáta, ktoré bude používateľ pravdepodobne v blízkej budúcnosti potrebovať, ešte predtým, ako ich explicitne požiada. To môže drasticky zlepšiť vnímaný výkon.
S knižnicami podporujúcimi Suspense sa prednačítavanie stáva bezproblémovým. Môžete spustiť načítavanie dát pri interakciách používateľa, ktoré okamžite nemenia UI, ako je napríklad prejdenie myšou nad odkazom alebo tlačidlom.
import React, { Suspense } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
// Predpokladajme, že toto sú vaše volania API
const fetchProductById = async (id) => {
console.log(`Načítava sa produkt ${id}...`);
return new Promise(resolve => setTimeout(() => {
const products = {
'A001': { id: 'A001', name: 'Global Widget X', price: 29.99, description: 'Všestranný widget pre medzinárodné použitie.' },
'B002': { id: 'B002', name: 'Universal Gadget Y', price: 149.99, description: 'Špičkový gadget, obľúbený po celom svete.' },
};
resolve(products[id]);
}, 1000));
};
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true, // Štandardne povoliť Suspense pre všetky dopyty
},
},
});
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) => {
// Prednačítať dáta, keď používateľ prejde myšou ponad odkaz na produkt
queryClient.prefetchQuery({
queryKey: ['product', productId],
queryFn: () => fetchProductById(productId),
});
console.log(`Prednačítava sa produkt ${productId}`);
};
return (
<div>
<h2>Dostupné produkty:</h2>
<ul>
<li>
<a href="#" onMouseEnter={() => handleProductHover('A001')}
onClick={(e) => { e.preventDefault(); /* Prejdite alebo zobrazte detaily */ }}
>Global Widget X (A001)</a>
</li>
<li>
<a href="#" onMouseEnter={() => handleProductHover('B002')}
onClick={(e) => { e.preventDefault(); /* Prejdite alebo zobrazte detaily */ }}
>Universal Gadget Y (B002)</a>
</li>
</ul>
<p>Prejdite myšou ponad odkaz na produkt, aby ste videli prednačítavanie v akcii. Pre sledovanie otvorte záložku siete.</p>
</div>
);
}
function App() {
const [showProductA, setShowProductA] = React.useState(false);
const [showProductB, setShowProductB] = React.useState(false);
return (
<QueryClientProvider client={queryClient}>
<h1>Prednačítavanie s React Suspense (React Query)</h1>
<ProductList />
<button onClick={() => setShowProductA(true)}>Zobraziť Global Widget X</button>
<button onClick={() => setShowProductB(true)}>Zobraziť Universal Gadget Y</button>
{showProductA && (
<Suspense fallback={<p>Načítava sa Global Widget X...</p>}>
<ProductDetails productId="A001" />
</Suspense>
)}
{showProductB && (
<Suspense fallback={<p>Načítava sa Universal Gadget Y...</p>}>
<ProductDetails productId="B002" />
</Suspense>
)}
</QueryClientProvider>
);
}
V tomto príklade spustí prejdenie myšou nad odkazom na produkt `queryClient.prefetchQuery`, čo iniciuje načítanie dát na pozadí. Ak používateľ potom klikne na tlačidlo na zobrazenie detailov produktu a dáta sú už v keši z prednačítania, komponent sa renderuje okamžite bez pozastavenia. Ak prednačítanie stále prebieha alebo nebolo iniciované, Suspense zobrazí fallback, kým dáta nebudú pripravené.
Spracovanie chýb so Suspense a Error Boundaries
Zatiaľ čo Suspense spracováva stav 'načítavania' zobrazením fallbacku, priamo nespracováva 'chybové' stavy. Ak je promise vyvolaný pozastavujúcim komponentom zamietnutý (t.j. načítavanie dát zlyhá), táto chyba sa bude šíriť hore stromom komponentov. Na elegantné spracovanie týchto chýb a zobrazenie vhodného UI musíte použiť Error Boundaries (hranice chýb).
Error Boundary je React komponent, ktorý implementuje buď životné cykly componentDidCatch
alebo static getDerivedStateFromError
. Zachytáva JavaScriptové chyby kdekoľvek vo svojom podstromu komponentov, vrátane chýb vyvolaných promises, ktoré by Suspense normálne zachytil, keby boli čakajúce.
import React, { Suspense, useState } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
// --- Komponent Error Boundary --- //
class MyErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
// Aktualizuje stav, aby nasledujúce renderovanie zobrazilo záložné UI.
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Chybu môžete tiež zaznamenať do služby na hlásenie chýb
console.error("Bola zachytená chyba:", error, errorInfo);
}
render() {
if (this.state.hasError) {
// Môžete renderovať akékoľvek vlastné záložné UI
return (
<div style={{"border": "2px solid red", "padding": "20px", "margin": "20px 0", "background": "#ffe0e0"}}>
<h2>Niečo sa pokazilo!</h2>
<p>{this.state.error && this.state.error.message}</p>
<p>Skúste obnoviť stránku alebo kontaktujte podporu.</p>
<button onClick={() => this.setState({ hasError: false, error: null })}>Skúsiť znova</button>
</div>
);
}
return this.props.children;
}
}
// --- Načítavanie dát (s potenciálom chyby) --- //
const fetchItemById = async (id) => {
console.log(`Pokus o načítanie položky ${id}...`);
return new Promise((resolve, reject) => setTimeout(() => {
if (id === 'error-item') {
reject(new Error('Nepodarilo sa načítať položku: Sieť je nedostupná alebo položka nebola nájdená.'));
} else if (id === 'slow-item') {
resolve({ id: 'slow-item', name: 'Doručené pomaly', data: 'Táto položka trvala dlhšie, ale dorazila!', status: 'success' });
} else {
resolve({ id, name: `Položka ${id}`, data: `Dáta pre položku ${id}` });
}
}, id === 'slow-item' ? 3000 : 800));
};
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true,
retry: false, // Pre demonštráciu, vypnite opakovanie, aby bola 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>Meno: {item.name}</p>
<p>Dáta: {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čítať normálnu položku</button>
<button onClick={() => setFetchType('slow-item')}>Načítať pomalú položku</button>
<button onClick={() => setFetchType('error-item')}>Načítať chybnú položku</button>
</div>
<MyErrorBoundary>
<Suspense fallback={<p>Načítava sa položka cez Suspense...</p>}>
<DisplayItem itemId={fetchType} />
</Suspense>
</MyErrorBoundary>
</QueryClientProvider>
);
}
Obalením vašej hranice Suspense (alebo komponentov, ktoré sa môžu pozastaviť) do Error Boundary zabezpečíte, že sieťové zlyhania alebo chyby servera počas načítavania dát budú zachytené a elegantne spracované, čím zabránite pádu celej aplikácie. To poskytuje robustný a používateľsky prívetivý zážitok, umožňujúci používateľom pochopiť problém a potenciálne to skúsiť znova.
Správa stavu a invalidácia dát so Suspense
Je dôležité objasniť, že React Suspense primárne rieši počiatočný stav načítavania asynchrónnych zdrojov. Vnútorne nespravuje keš na strane klienta, nerieši invalidáciu dát, ani neorganizuje mutácie (operácie create, update, delete) a ich následné aktualizácie UI.
Tu sa stávajú nepostrádateľnými knižnice na načítavanie dát podporujúce Suspense (React Query, SWR, Apollo Client, Relay). Dopĺňajú Suspense tým, že poskytujú:
- Robustné kešovanie: Udržiavajú sofistikovanú keš načítaných dát v pamäti, okamžite ich servírujú, ak sú dostupné, a spravujú revalidáciu na pozadí.
- Invalidácia a opätovné načítanie dát: Ponúkajú mechanizmy na označenie kešovaných dát ako 'zastaralých' a ich opätovné načítanie (napr. po mutácii, interakcii používateľa alebo pri zameraní okna).
- Optimistické aktualizácie: Pri mutáciách vám umožňujú okamžite aktualizovať UI (optimisticky) na základe očakávaného výsledku volania API a potom sa vrátiť späť, ak skutočné volanie API zlyhá.
- Globálna synchronizácia stavu: Zabezpečujú, že ak sa dáta zmenia v jednej časti vašej aplikácie, všetky komponenty zobrazujúce tieto dáta sa automaticky aktualizujú.
- Stavy načítavania a chýb pre mutácie: Zatiaľ čo
useQuery
sa môže pozastaviť,useMutation
zvyčajne poskytuje stavyisLoading
aisError
pre samotný proces mutácie, keďže mutácie sú často interaktívne a vyžadujú okamžitú spätnú väzbu.
Bez robustnej knižnice na načítavanie dát by implementácia týchto funkcií nad manuálnym správcom zdrojov pre Suspense bola významným podnikom, v podstate by ste si museli vytvoriť vlastný framework na načítavanie dát.
Praktické úvahy a osvedčené postupy
Prijatie Suspense pre načítavanie dát je významné architektonické rozhodnutie. Tu sú niektoré praktické úvahy pre globálnu aplikáciu:
1. Nie všetky dáta potrebujú Suspense
Suspense je ideálny pre kritické dáta, ktoré priamo ovplyvňujú počiatočné renderovanie komponentu. Pre nekritické dáta, načítavanie na pozadí alebo dáta, ktoré je možné načítať lenivo bez silného vizuálneho dopadu, môže byť stále vhodný tradičný useEffect
alebo pred-renderovanie. Nadmerné používanie Suspense môže viesť k menej granulárnemu zážitku z načítavania, keďže jedna hranica Suspense čaká na vyriešenie *všetkých* svojich detí.
2. Granularita hraníc Suspense
Umiestňujte svoje hranice <Suspense>
premyslene. Jedna veľká hranica na vrchu vašej aplikácie môže skryť celú stránku za spinnerom, čo môže byť frustrujúce. Menšie, granulárnejšie hranice umožňujú rôznym častiam vašej stránky načítať sa nezávisle, poskytujúc progresívnejší a responzívnejší zážitok. Napríklad, hranica okolo komponentu profilu používateľa a ďalšia okolo zoznamu odporúčaných produktov.
<div>
<h1>Stránka produktu</h1>
<Suspense fallback={<p>Načítavajú sa hlavné detaily produktu...</p>}>
<ProductDetails id="prod123" />
</Suspense>
<hr />
<h2>Súvisiace produkty</h2>
<Suspense fallback={<p>Načítavajú sa súvisiace produkty...</p>}>
<RelatedProducts category="electronics" />
</Suspense>
</div>
Tento prístup znamená, že používatelia môžu vidieť hlavné detaily produktu, aj keď sa súvisiace produkty stále načítavajú.
3. Server-Side Rendering (SSR) a streamovanie HTML
Nové streamovacie SSR API v React 18 (renderToPipeableStream
) sa plne integrujú so Suspense. To umožňuje vášmu serveru posielať HTML hneď, ako je pripravené, aj keď sa časti stránky (ako komponenty závislé od dát) stále načítavajú. Server môže streamovať zástupný symbol (z fallbacku Suspense) a potom streamovať skutočný obsah, keď sa dáta vyriešia, bez potreby úplného pre-renderovania na strane klienta. To výrazne zlepšuje vnímaný výkon načítavania pre globálnych používateľov s rôznymi podmienkami siete.
4. Postupné prijatie
Nemusíte prepisovať celú svoju aplikáciu, aby ste mohli používať Suspense. Môžete ho zavádzať postupne, začínajúc novými funkciami alebo komponentmi, ktoré by najviac profitovali z jeho deklaratívnych vzorov načítavania.
5. Nástroje a ladenie
Zatiaľ čo Suspense zjednodušuje logiku komponentov, ladenie môže byť iné. React DevTools poskytujú prehľad o hraniciach Suspense a ich stavoch. Oboznámte sa s tým, ako vaša zvolená knižnica na načítavanie dát odhaľuje svoj interný stav (napr. React Query Devtools).
6. Časové limity pre záložné UI Suspense
Pri veľmi dlhých časoch načítavania by ste mohli chcieť zaviesť časový limit pre váš fallback Suspense, alebo po určitej dobe prejsť na podrobnejší indikátor načítavania. Hooky useDeferredValue
a useTransition
v React 18 môžu pomôcť spravovať tieto jemnejšie stavy načítavania, umožňujúc vám zobraziť 'starú' verziu UI, zatiaľ čo sa načítavajú nové dáta, alebo odložiť neurgentné aktualizácie.
Budúcnosť načítavania dát v Reacte: React Server Components a ďalej
Cesta načítavania dát v Reacte nekončí pri Suspense na strane klienta. React Server Components (RSC) predstavujú významnú evolúciu, sľubujúc stieranie hraníc medzi klientom a serverom a ďalšiu optimalizáciu načítavania dát.
- React Server Components (RSC): Tieto komponenty sa renderujú na serveri, priamo načítavajú svoje dáta a potom posielajú do prehliadača iba potrebné HTML a JavaScript na strane klienta. Tým sa eliminujú vodopády na strane klienta, znižuje sa veľkosť balíkov a zlepšuje sa počiatočný výkon načítavania. RSC pracujú ruka v ruke so Suspense: serverové komponenty sa môžu pozastaviť, ak ich dáta nie sú pripravené, a server môže streamovať fallback Suspense klientovi, ktorý sa potom nahradí, keď sa dáta vyriešia. Toto je prevratná zmena pre aplikácie s komplexnými požiadavkami na dáta, ponúkajúca plynulý a vysoko výkonný zážitok, čo je obzvlášť prínosné pre používateľov v rôznych geografických oblastiach s rôznou latenciou.
- Jednotné načítavanie dát: Dlhodobá vízia pre React zahŕňa jednotný prístup k načítavaniu dát, kde základný framework alebo úzko integrované riešenia poskytujú prvotriednu podporu pre načítavanie dát ako na serveri, tak aj na klientovi, všetko koordinované pomocou Suspense.
- Pokračujúca evolúcia knižníc: Knižnice na načítavanie dát sa budú naďalej vyvíjať, ponúkajúc ešte sofistikovanejšie funkcie pre kešovanie, invalidáciu a real-time aktualizácie, stavajúc na základných schopnostiach Suspense.
Ako React pokračuje v dospievaní, Suspense bude čoraz centrálnejším dielom skladačky pre budovanie vysoko výkonných, používateľsky prívetivých a udržiavateľných aplikácií. Posúva vývojárov smerom k deklaratívnejšiemu a odolnejšiemu spôsobu narábania s asynchrónnymi operáciami, presúvajúc zložitosť z jednotlivých komponentov do dobre spravovanej dátovej vrstvy.
Záver
React Suspense, pôvodne funkcia pre delenie kódu, sa rozvinul do transformačného nástroja pre načítavanie dát. Prijatím vzoru Fetch-As-You-Render a využitím knižníc podporujúcich Suspense môžu vývojári výrazne zlepšiť používateľský zážitok svojich aplikácií, eliminovať vodopády načítavania, zjednodušiť logiku komponentov a poskytovať plynulé, koordinované stavy načítavania. V kombinácii s Error Boundaries pre robustné spracovanie chýb a budúcim prísľubom React Server Components nám Suspense umožňuje budovať aplikácie, ktoré sú nielen výkonné a odolné, ale aj vnútorne príjemnejšie pre používateľov po celom svete. Prechod na paradigmu načítavania dát riadenú Suspense si vyžaduje koncepčnú úpravu, ale výhody v oblasti zrozumiteľnosti kódu, výkonu a spokojnosti používateľov sú podstatné a stoja za investíciu.