Atraskite „React Suspense“ duomenų gavimui ne tik kodo padalijimui. Supraskite „Fetch-As-You-Render“, klaidų valdymą ir ateities globalių programų modelius.
React Suspense išteklių įkėlimas: šiuolaikinių duomenų gavimo modelių įsisavinimas
Dinamiškame žiniatinklio kūrimo pasaulyje svarbiausia yra vartotojo patirtis (UX). Tikimasi, kad programos bus greitos, jautrios ir malonios naudoti, nepriklausomai nuo tinklo sąlygų ar įrenginio galimybių. „React“ kūrėjams tai dažnai reiškia sudėtingą būsenos valdymą, sudėtingus įkėlimo indikatorius ir nuolatinę kovą su duomenų gavimo kriokliais. Pristatome „React Suspense“ – galingą, nors dažnai neteisingai suprantamą funkciją, skirtą iš esmės pakeisti, kaip tvarkome asinchronines operacijas, ypač duomenų gavimą.
Iš pradžių pristatyta kodo padalijimui su React.lazy()
, tikrasis „Suspense“ potencialas slypi jo gebėjime organizuoti *bet kokio* asinchroninio ištekliaus, įskaitant duomenis iš API, įkėlimą. Šis išsamus vadovas gilinsis į „React Suspense“ išteklių įkėlimui, tyrinės jo pagrindines koncepcijas, esminius duomenų gavimo modelius ir praktinius aspektus kuriant našias ir atsparias globalias programas.
Duomenų gavimo evoliucija „React“: nuo imperatyvaus iki deklaratyvaus
Daugelį metų duomenų gavimas „React“ komponentuose daugiausia rėmėsi įprastu modeliu: naudojant useEffect
„hook'ą“ inicijuoti API iškvietimą, valdant įkėlimo ir klaidų būsenas su useState
ir sąlygiškai atvaizduojant remiantis šiomis būsenomis. Nors ir funkcionalus, šis požiūris dažnai sukeldavo keletą iššūkių:
- Įkėlimo būsenos plitimas: Beveik kiekvienam komponentui, reikalaujančiam duomenų, reikėjo savo
isLoading
,isError
irdata
būsenų, o tai vedė prie pasikartojančio šabloninio kodo. - Kriokliai ir lenktynių sąlygos: Įdėtieji komponentai, gaunantys duomenis, dažnai sukeldavo nuoseklias užklausas (krioklius), kai tėvinis komponentas gaudavo duomenis, tada atvaizduodavo, o tada vaiko komponentas gaudavo savo duomenis ir t.t. Tai didino bendrą įkėlimo laiką. Lenktynių sąlygos taip pat galėjo atsirasti, kai buvo inicijuojamos kelios užklausos, o atsakymai gaunami ne eilės tvarka.
- Sudėtingas klaidų valdymas: Klaidų pranešimų ir atkūrimo logikos paskirstymas tarp daugybės komponentų galėjo būti sudėtingas, reikalaujantis savybių perdavimo (prop drilling) ar globalių būsenos valdymo sprendimų.
- Nemaloni vartotojo patirtis: Daugybė besisukančių indikatorių, kurie atsiranda ir dingsta, arba staigūs turinio poslinkiai (layout shifts) galėjo sukurti nemalonią patirtį vartotojams.
- Savybių perdavimas (prop drilling) duomenims ir būsenai: Gautų duomenų ir susijusių įkėlimo/klaidų būsenų perdavimas per kelis komponentų lygius tapo dažnu sudėtingumo šaltiniu.
Panagrinėkime tipišką duomenų gavimo scenarijų be „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>Loading user profile...</p>;
}
if (error) {
return <p style={"color: red;"}>Error: {error.message}</p>;
}
if (!user) {
return <p>No user data available.</p>;
}
return (
<div>
<h2>User: {user.name}</h2>
<p>Email: {user.email}</p>
<!-- More user details -->
</div>
);
}
function App() {
return (
<div>
<h1>Welcome to the Application</h1>
<UserProfile userId={"123"} />
</div>
);
}
Šis modelis yra visur paplitęs, tačiau jis priverčia komponentą valdyti savo asinchroninę būseną, dažnai sukuriant glaudų ryšį tarp vartotojo sąsajos ir duomenų gavimo logikos. „Suspense“ siūlo deklaratyvesnę ir supaprastintą alternatyvą.
„React Suspense“ supratimas ne tik kodo padalijimui
Dauguma kūrėjų pirmą kartą susiduria su „Suspense“ per React.lazy()
kodo padalijimui, kai leidžiama atidėti komponento kodo įkėlimą, kol jis bus reikalingas. Pavyzdžiui:
import React, { Suspense, lazy } from 'react';
const LazyComponent = lazy(() => import('./MyHeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading component...</div>}>
<LazyComponent />
</Suspense>
);
}
Šiame scenarijuje, jei MyHeavyComponent
dar nėra įkeltas, <Suspense>
riba pagaus pažadą (promise), kurį išmeta lazy()
, ir rodys fallback
, kol komponento kodas bus paruoštas. Svarbiausia įžvalga čia yra ta, kad „Suspense“ veikia gaudydamas pažadus, išmestus atvaizdavimo metu.
Šis mechanizmas nėra išskirtinis tik kodo įkėlimui. Bet kuri funkcija, iškviesta atvaizdavimo metu, kuri išmeta pažadą (pvz., nes išteklius dar neprieinamas), gali būti pagauta „Suspense“ ribos aukščiau komponentų medyje. Kai pažadas išsipildo, „React“ bando iš naujo atvaizduoti komponentą, ir jei išteklius dabar yra prieinamas, atsarginis turinys yra paslepiamas, o tikrasis turinys yra rodomas.
Pagrindinės „Suspense“ koncepcijos duomenų gavimui
Norėdami pasinaudoti „Suspense“ duomenų gavimui, turime suprasti keletą pagrindinių principų:
1. Pažado išmetimas (Throwing a Promise)
Skirtingai nuo tradicinio asinchroninio kodo, kuris naudoja async/await
pažadams išspręsti, „Suspense“ remiasi funkcija, kuri *išmeta* pažadą, jei duomenys nėra paruošti. Kai „React“ bando atvaizduoti komponentą, kuris kviečia tokią funkciją, o duomenys vis dar laukiami, pažadas yra išmetamas. „React“ tada „pristabdo“ to komponento ir jo vaikų atvaizdavimą, ieškodamas artimiausios <Suspense>
ribos.
2. „Suspense“ riba
Komponentas <Suspense>
veikia kaip klaidų riba pažadams. Jis priima fallback
savybę, kuri yra vartotojo sąsaja, kurią reikia atvaizduoti, kol bet kuris iš jo vaikų (ar jų palikuonių) yra sustabdytas (t. y., meta pažadą). Kai visi pažadai, išmesti jo medyje, išsipildo, atsarginis turinys pakeičiamas tikruoju turiniu.
Viena „Suspense“ riba gali valdyti kelias asinchronines operacijas. Pavyzdžiui, jei turite du komponentus toje pačioje <Suspense>
riboje ir kiekvienam reikia gauti duomenis, atsarginis turinys bus rodomas, kol bus baigti *abu* duomenų gavimo procesai. Tai leidžia išvengti dalinės vartotojo sąsajos rodymo ir suteikia labiau koordinuotą įkėlimo patirtį.
3. Podėlio/Išteklių valdytojas (Userland Responsibility)
Svarbu tai, kad pats „Suspense“ netvarko duomenų gavimo ar podėliavimo. Tai tik koordinavimo mechanizmas. Kad „Suspense“ veiktų su duomenų gavimu, jums reikia sluoksnio, kuris:
- Inicijuoja duomenų gavimą.
- Į podėlį įtraukia rezultatą (išspręstus duomenis arba laukiantį pažadą).
- Pateikia sinchroninį
read()
metodą, kuris arba grąžina podėlyje esančius duomenis iš karto (jei yra), arba išmeta laukiantį pažadą (jei ne).
Šis „išteklių valdytojas“ paprastai yra įgyvendinamas naudojant paprastą podėlį (pvz., „Map“ ar objektą), kad būtų saugoma kiekvieno ištekliaus būsena (laukia, išspręsta ar klaida). Nors demonstraciniais tikslais galite tai sukurti rankiniu būdu, realioje programoje naudotumėte tvirtą duomenų gavimo biblioteką, kuri integruojasi su „Suspense“.
4. Lygiagretusis režimas („React 18“ patobulinimai)
Nors „Suspense“ galima naudoti senesnėse „React“ versijose, visa jo galia atsiskleidžia su „Concurrent React“ (numatytasis „React 18“ su createRoot
). Lygiagretusis režimas leidžia „React“ pertraukti, pristabdyti ir tęsti atvaizdavimo darbus. Tai reiškia:
- Neblokuojantys UI atnaujinimai: Kai „Suspense“ rodo atsarginį turinį, „React“ gali toliau atvaizduoti kitas UI dalis, kurios nėra sustabdytos, arba net paruošti naują UI fone, neblokuodamas pagrindinės gijos.
- Perėjimai (Transitions): Naujos API, tokios kaip
useTransition
, leidžia pažymėti tam tikrus atnaujinimus kaip „perėjimus“, kuriuos „React“ gali pertraukti ir padaryti mažiau skubiais, užtikrinant sklandesnius UI pokyčius duomenų gavimo metu.
Duomenų gavimo modeliai su „Suspense“
Panagrinėkime duomenų gavimo modelių evoliuciją atsiradus „Suspense“.
1 modelis: Gauti-tada-atvaizduoti (tradicinis su „Suspense“ apgaubimu)
Tai klasikinis požiūris, kai duomenys gaunami, ir tik tada komponentas atvaizduojamas. Nors tiesiogiai nenaudojamas „išmesti pažadą“ mechanizmas duomenims, galite apgaubti komponentą, kuris *galiausiai* atvaizduoja duomenis, „Suspense“ riboje, kad pateiktumėte atsarginį turinį. Tai labiau susiję su „Suspense“ naudojimu kaip bendro įkėlimo UI orkestratoriumi komponentams, kurie galiausiai tampa paruošti, net jei jų vidinis duomenų gavimas vis dar pagrįstas tradiciniu 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>Loading user details...</p>;
}
return (
<div>
<h3>User: {user.name}</h3>
<p>Email: {user.email}</p>
</div>
);
}
function App() {
return (
<div>
<h1>Fetch-Then-Render Example</h1>
<Suspense fallback={<div>Overall page loading...</div>}>
<UserDetails userId={"1"} />
</Suspense>
</div>
);
}
Privalumai: Paprasta suprasti, atgalinis suderinamumas. Gali būti naudojamas kaip greitas būdas pridėti globalią įkėlimo būseną.
Trūkumai: Nepašalina šabloninio kodo UserDetails
viduje. Vis dar linkęs į krioklius, jei komponentai gauna duomenis nuosekliai. Iš tikrųjų nenaudoja „Suspense“ „išmesti-ir-pagauti“ mechanizmo patiems duomenims.
2 modelis: Atvaizduoti-tada-gauti (gavimas atvaizdavimo metu, netinka produkcijai)
Šis modelis daugiausia skirtas iliustruoti, ko nedaryti su „Suspense“ tiesiogiai, nes tai gali sukelti begalines kilpas ar našumo problemas, jei nebus kruopščiai valdoma. Tai apima bandymą gauti duomenis arba kviesti sustabdančią funkciją tiesiogiai komponento atvaizdavimo fazėje, *be* tinkamo podėliavimo mechanizmo.
// NENAUDOKITE TO PRODUKCIJOJE BE TINKAMO PODĖLIAVIMO SLUOKSNIO
// Tai tik iliustracija, kaip konceptualiai galėtų veikti tiesioginis 'throw'.
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; // Čia įsijungia Suspense
}
function UserDetailsBadExample({ userId }) {
const user = fetchDataSynchronously(`/api/users/${userId}`);
return (
<div>
<h3>User: {user.name}</h3>
<p>Email: {user.email}</p>
</div>
);
}
function App() {
return (
<div>
<h1>Render-Then-Fetch (Illustrative, NOT Recommended Directly)</h1>
<Suspense fallback={<div>Loading user...</div>}>
<UserDetailsBadExample userId={"2"} />
</Suspense>
</div>
);
}
Privalumai: Parodo, kaip komponentas gali tiesiogiai „prašyti“ duomenų ir sustabdyti vykdymą, jei jie neparuošti.
Trūkumai: Labai problematiška produkcijai. Ši rankinė, globali fetchedData
ir dataPromise
sistema yra primityvi, netvarko kelių užklausų, invalidavimo ar klaidų būsenų tvirtai. Tai yra primityvi „išmesti-pažadą“ koncepcijos iliustracija, o ne modelis, kurį reikėtų taikyti.
3 modelis: Gauti-atvaizduojant (Fetch-As-You-Render – idealus „Suspense“ modelis)
Tai yra paradigmos pokytis, kurį „Suspense“ iš tiesų įgalina duomenų gavimui. Užuot laukus, kol komponentas bus atvaizduotas, prieš pradedant gauti jo duomenis, arba gaunant visus duomenis iš anksto, „Gauti-atvaizduojant“ reiškia, kad pradedate gauti duomenis *kuo greičiau*, dažnai *prieš* arba *lygiagrečiai su* atvaizdavimo procesu. Tada komponentai „skaito“ duomenis iš podėlio, ir jei duomenys dar neparuošti, jie sustabdo vykdymą. Pagrindinė idėja yra atskirti duomenų gavimo logiką nuo komponento atvaizdavimo logikos.
Norint įgyvendinti „Gauti-atvaizduojant“, jums reikia mechanizmo, kuris:
- Inicijuoja duomenų gavimą ne komponento atvaizdavimo funkcijoje (pvz., kai įeinama į maršrutą arba paspaudžiamas mygtukas).
- Saugo pažadą arba išspręstus duomenis podėlyje.
- Suteikia būdą komponentams „skaityti“ iš šio podėlio. Jei duomenys dar neprieinami, skaitymo funkcija išmeta laukiantį pažadą.
Šis modelis sprendžia krioklio problemą. Jei dviem skirtingiems komponentams reikia duomenų, jų užklausos gali būti inicijuotos lygiagrečiai, o vartotojo sąsaja pasirodys tik tada, kai *abu* bus paruošti, koordinuojami vienos „Suspense“ ribos.
Rankinis įgyvendinimas (supratimui)
Norėdami suprasti pagrindinius mechanizmus, sukurkime supaprastintą rankinį išteklių valdytoją. Realioje programoje naudotumėte specializuotą biblioteką.
import React, { Suspense } from 'react';
// --- Paprastas podėlio/išteklių valdytojas --- //
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);
}
// --- Duomenų gavimo funkcijos --- //
const fetchUserById = (id) => {
console.log(`Fetching user ${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(`Fetching posts for user ${userId}...`);
return new Promise(resolve => setTimeout(() => {
const posts = {
'1': [{ id: 'p1', title: 'My First Post' }, { id: 'p2', title: 'Travel Adventures' }],
'2': [{ id: 'p3', title: 'Coding Insights' }],
'3': [{ id: 'p4', title: 'Global Trends' }, { id: 'p5', title: 'Local Cuisine' }]
};
resolve(posts[userId] || []);
}, 2000));
};
// --- Komponentai --- //
function UserProfile({ userId }) {
const userResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
const user = userResource.read(); // Tai sustabdys vykdymą, jei vartotojo duomenys neparuošti
return (
<div>
<h3>User: {user.name}</h3>
<p>Email: {user.email}</p>
</div>
);
}
function UserPosts({ userId }) {
const postsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
const posts = postsResource.read(); // Tai sustabdys vykdymą, jei įrašų duomenys neparuošti
return (
<div>
<h4>Posts by {userId}:</h4>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
{posts.length === 0 && <li>No posts found.</li>}
</ul>
</div>
);
}
// --- Programa --- //
let initialUserResource = null;
let initialPostsResource = null;
function prefetchDataForUser(userId) {
initialUserResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
initialPostsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
}
// Iš anksto gauti kai kuriuos duomenis dar prieš atvaizduojant App komponentą
prefetchDataForUser('1');
function App() {
return (
<div>
<h1>Fetch-As-You-Render with Suspense</h1>
<p>This demonstrates how data fetching can happen in parallel, coordinated by Suspense.</p>
<Suspense fallback={<div>Loading user profile and posts...</div>}>
<UserProfile userId={"1"} />
<UserPosts userId={"1"} />
</Suspense>
<h2>Another Section</h2>
<Suspense fallback={<div>Loading different user...</div>}>
<UserProfile userId={"2"} />
</Suspense>
</div>
);
}
Šiame pavyzdyje:
createResource
irfetchData
funkcijos sukuria pagrindinį podėliavimo mechanizmą.- Kai
UserProfile
arbaUserPosts
kviečiaresource.read()
, jie arba iškart gauna duomenis, arba yra išmetamas pažadas. - Artimiausia
<Suspense>
riba pagauna pažadą(-us) ir rodo savo atsarginį turinį. - Svarbiausia, kad galime iškviesti
prefetchDataForUser('1')
*prieš* atvaizduojantApp
komponentą, leisdami duomenų gavimui prasidėti dar anksčiau.
Bibliotekos, skirtos „Gauti-atvaizduojant“
Sukurti ir prižiūrėti tvirtą išteklių valdytoją rankiniu būdu yra sudėtinga. Laimei, kelios brandžios duomenų gavimo bibliotekos pritaikė arba pritaiko „Suspense“, siūlydamos mūšyje išbandytus sprendimus:
- React Query (TanStack Query): Siūlo galingą duomenų gavimo ir podėliavimo sluoksnį su „Suspense“ palaikymu. Ji teikia „hook'us“, tokius kaip
useQuery
, kurie gali sustabdyti vykdymą. Puikiai tinka REST API. - SWR (Stale-While-Revalidate): Kita populiari ir lengva duomenų gavimo biblioteka, kuri visiškai palaiko „Suspense“. Idealiai tinka REST API, ji orientuota į greitą duomenų pateikimą (pasenusių) ir vėlesnį jų patvirtinimą fone.
- Apollo Client: Išsamus GraphQL klientas, turintis tvirtą „Suspense“ integraciją GraphQL užklausoms ir mutacijoms.
- Relay: Paties „Facebook“ GraphQL klientas, sukurtas nuo pagrindų „Suspense“ ir „Concurrent React“. Jis reikalauja specifinės GraphQL schemos ir kompiliavimo žingsnio, tačiau siūlo neprilygstamą našumą ir duomenų nuoseklumą.
- Urql: Lengvas ir labai pritaikomas GraphQL klientas su „Suspense“ palaikymu.
Šios bibliotekos abstrahuoja išteklių kūrimo ir valdymo, podėliavimo, pakartotinio patvirtinimo, optimistinių atnaujinimų ir klaidų valdymo sudėtingumą, todėl įgyvendinti „Gauti-atvaizduojant“ tampa daug lengviau.
4 modelis: Išankstinis gavimas su „Suspense“ palaikančiomis bibliotekomis
Išankstinis gavimas (prefetching) yra galinga optimizacija, kai aktyviai gaunate duomenis, kurių vartotojui greičiausiai prireiks artimiausiu metu, dar prieš jam aiškiai jų paprašant. Tai gali drastiškai pagerinti suvokiamą našumą.
Su „Suspense“ palaikančiomis bibliotekomis išankstinis gavimas tampa sklandus. Galite inicijuoti duomenų gavimą reaguodami į vartotojo veiksmus, kurie iš karto nekeičia vartotojo sąsajos, pavyzdžiui, užvedus pelės žymeklį ant nuorodos ar mygtuko.
import React, { Suspense } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
// Assume these are your API calls
const fetchProductById = async (id) => {
console.log(`Fetching product ${id}...`);
return new Promise(resolve => setTimeout(() => {
const products = {
'A001': { id: 'A001', name: 'Global Widget X', price: 29.99, description: 'A versatile widget for international use.' },
'B002': { id: 'B002', name: 'Universal Gadget Y', price: 149.99, description: 'Cutting-edge gadget, loved worldwide.' },
};
resolve(products[id]);
}, 1000));
};
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true, // Enable Suspense for all queries by default
},
},
});
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>Price: ${product.price.toFixed(2)}</p>
<p>{product.description}</p>
</div>
);
}
function ProductList() {
const handleProductHover = (productId) => {
// Prefetch data when a user hovers over a product link
queryClient.prefetchQuery({
queryKey: ['product', productId],
queryFn: () => fetchProductById(productId),
});
console.log(`Prefetching product ${productId}`);
};
return (
<div>
<h2>Available Products:</h2>
<ul>
<li>
<a href="#" onMouseEnter={() => handleProductHover('A001')}
onClick={(e) => { e.preventDefault(); /* Navigate or show details */ }}
>Global Widget X (A001)</a>
</li>
<li>
<a href="#" onMouseEnter={() => handleProductHover('B002')}
onClick={(e) => { e.preventDefault(); /* Navigate or show details */ }}
>Universal Gadget Y (B002)</a>
</li>
</ul>
<p>Hover over a product link to see prefetching in action. Open network tab to observe.</p>
</div>
);
}
function App() {
const [showProductA, setShowProductA] = React.useState(false);
const [showProductB, setShowProductB] = React.useState(false);
return (
<QueryClientProvider client={queryClient}>
<h1>Prefetching with React Suspense (React Query)</h1>
<ProductList />
<button onClick={() => setShowProductA(true)}>Show Global Widget X</button>
<button onClick={() => setShowProductB(true)}>Show Universal Gadget Y</button>
{showProductA && (
<Suspense fallback={<p>Loading Global Widget X...</p>}>
<ProductDetails productId="A001" />
</Suspense>
)}
{showProductB && (
<Suspense fallback={<p>Loading Universal Gadget Y...</p>}>
<ProductDetails productId="B002" />
</Suspense>
)}
</QueryClientProvider>
);
}
Šiame pavyzdyje, užvedus pelės žymeklį ant produkto nuorodos, paleidžiama `queryClient.prefetchQuery`, kuri inicijuoja duomenų gavimą fone. Jei vartotojas tada paspaudžia mygtuką, kad parodytų produkto detales, ir duomenys jau yra podėlyje iš išankstinio gavimo, komponentas bus atvaizduotas iš karto be sustabdymo. Jei išankstinis gavimas dar vyksta arba nebuvo inicijuotas, „Suspense“ parodys atsarginį turinį, kol duomenys bus paruošti.
Klaidų valdymas su „Suspense“ ir klaidų ribomis (Error Boundaries)
Nors „Suspense“ valdo „įkėlimo“ būseną rodydamas atsarginį turinį, jis tiesiogiai nevaldo „klaidos“ būsenų. Jei pažadas, išmestas sustabdančio komponento, yra atmetamas (t. y., duomenų gavimas nepavyksta), ši klaida bus perduota aukštyn komponentų medžiu. Norėdami grakščiai tvarkyti šias klaidas ir parodyti tinkamą vartotojo sąsają, turite naudoti klaidų ribas (Error Boundaries).
Klaidų riba yra „React“ komponentas, kuris įgyvendina arba componentDidCatch
, arba static getDerivedStateFromError
gyvavimo ciklo metodus. Ji pagauna „JavaScript“ klaidas bet kurioje savo vaikų komponentų medžio vietoje, įskaitant klaidas, kurias išmeta pažadai, kuriuos „Suspense“ paprastai pagautų, jei jie būtų laukiami.
import React, { Suspense, useState } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
// --- Error Boundary Component --- //
class MyErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
console.error("Caught an error:", error, errorInfo);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return (
<div style={{"border": "2px solid red", "padding": "20px", "margin": "20px 0", "background": "#ffe0e0"}}>
<h2>Something went wrong!</h2>
<p>{this.state.error && this.state.error.message}</p>
<p>Please try refreshing the page or contact support.</p>
<button onClick={() => this.setState({ hasError: false, error: null })}>Try Again</button>
</div>
);
}
return this.props.children;
}
}
// --- Data Fetching (with potential for error) --- //
const fetchItemById = async (id) => {
console.log(`Attempting to fetch item ${id}...`);
return new Promise((resolve, reject) => setTimeout(() => {
if (id === 'error-item') {
reject(new Error('Failed to load item: Network unreachable or item not found.'));
} else if (id === 'slow-item') {
resolve({ id: 'slow-item', name: 'Delivered Slowly', data: 'This item took a while but arrived!', status: 'success' });
} else {
resolve({ id, name: `Item ${id}`, data: `Data for item ${id}` });
}
}, id === 'slow-item' ? 3000 : 800));
};
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true,
retry: false, // For demonstration, disable retry so error is immediate
},
},
});
function DisplayItem({ itemId }) {
const { data: item } = useQuery({
queryKey: ['item', itemId],
queryFn: () => fetchItemById(itemId),
});
return (
<div>
<h3>Item Details:</h3>
<p>ID: {item.id}</p>
<p>Name: {item.name}</p>
<p>Data: {item.data}</p>
</div>
);
}
function App() {
const [fetchType, setFetchType] = useState('normal-item');
return (
<QueryClientProvider client={queryClient}>
<h1>Suspense and Error Boundaries</h1>
<div>
<button onClick={() => setFetchType('normal-item')}>Fetch Normal Item</button>
<button onClick={() => setFetchType('slow-item')}>Fetch Slow Item</button>
<button onClick={() => setFetchType('error-item')}>Fetch Error Item</button>
</div>
<MyErrorBoundary>
<Suspense fallback={<p>Loading item via Suspense...</p>}>
<DisplayItem itemId={fetchType} />
</Suspense>
</MyErrorBoundary>
</QueryClientProvider>
);
}
Apgaubdami savo „Suspense“ ribą (arba komponentus, kurie gali sustabdyti vykdymą) klaidų riba, užtikrinate, kad tinklo gedimai ar serverio klaidos duomenų gavimo metu būtų pagautos ir grakščiai sutvarkytos, užkertant kelią visos programos sutrikimui. Tai suteikia tvirtą ir vartotojui draugišką patirtį, leidžiančią vartotojams suprasti problemą ir galbūt bandyti iš naujo.
Būsenos valdymas ir duomenų invalidavimas su „Suspense“
Svarbu paaiškinti, kad „React Suspense“ pirmiausia sprendžia pradinę asinchroninių išteklių įkėlimo būseną. Jis savaime nevaldo kliento pusės podėlio, netvarko duomenų invalidavimo ar neorganizuoja mutacijų (kūrimo, atnaujinimo, trynimo operacijų) ir jų vėlesnių UI atnaujinimų.
Čia „Suspense“ palaikančios duomenų gavimo bibliotekos („React Query“, SWR, „Apollo Client“, „Relay“) tampa nepakeičiamos. Jos papildo „Suspense“ teikdamos:
- Tvirtas podėliavimas: Jos palaiko sudėtingą atmintyje esantį gautų duomenų podėlį, akimirksniu pateikdamos jį, jei jis yra, ir tvarkydamos foninį pakartotinį patvirtinimą.
- Duomenų invalidavimas ir pakartotinis gavimas: Jos siūlo mechanizmus, kaip pažymėti podėlyje esančius duomenis kaip „pasenusius“ ir juos iš naujo gauti (pvz., po mutacijos, vartotojo sąveikos ar lango fokusavimo).
- Optimistiniai atnaujinimai: Mutacijoms jos leidžia nedelsiant atnaujinti UI (optimistiškai) remiantis laukiamu API iškvietimo rezultatu, o vėliau atšaukti, jei tikrasis API iškvietimas nepavyksta.
- Globalios būsenos sinchronizavimas: Jos užtikrina, kad jei duomenys pasikeičia vienoje programos dalyje, visi komponentai, rodantys tuos duomenis, yra automatiškai atnaujinami.
- Įkėlimo ir klaidų būsenos mutacijoms: Nors
useQuery
gali sustabdyti vykdymą,useMutation
paprastai teikiaisLoading
irisError
būsenas pačiam mutacijos procesui, nes mutacijos dažnai yra interaktyvios ir reikalauja nedelsiamo grįžtamojo ryšio.
Be tvirtos duomenų gavimo bibliotekos, šių funkcijų įgyvendinimas ant rankinio „Suspense“ išteklių valdytojo būtų didelis uždavinys, iš esmės reikalaujantis sukurti savo duomenų gavimo sistemą.
Praktiniai aspektai ir gerosios praktikos
Priimti „Suspense“ duomenų gavimui yra svarbus architektūrinis sprendimas. Štai keletas praktinių aspektų globaliai programai:
1. Ne visiems duomenims reikia „Suspense“
„Suspense“ idealiai tinka kritiniams duomenims, kurie tiesiogiai veikia pradinį komponento atvaizdavimą. Nekritiniams duomenims, foniniams gavimams ar duomenims, kuriuos galima įkelti tingiai be didelio vizualinio poveikio, tradicinis useEffect
ar išankstinis atvaizdavimas vis dar gali būti tinkamas. Per didelis „Suspense“ naudojimas gali lemti mažiau detalizuotą įkėlimo patirtį, nes viena „Suspense“ riba laukia, kol *visi* jos vaikai bus išspręsti.
2. „Suspense“ ribų detalumas
Apgalvotai išdėstykite savo <Suspense>
ribas. Viena didelė riba programos viršuje gali paslėpti visą puslapį už besisukančio indikatoriaus, o tai gali būti nemalonu. Mažesnės, detalesnės ribos leidžia skirtingoms puslapio dalims įsikelti nepriklausomai, suteikiant laipsnišką ir jautresnę patirtį. Pavyzdžiui, viena riba aplink vartotojo profilio komponentą, o kita – aplink rekomenduojamų produktų sąrašą.
<div>
<h1>Product Page</h1>
<Suspense fallback={<p>Loading main product details...</p>}>
<ProductDetails id="prod123" />
</Suspense>
<hr />
<h2>Related Products</h2>
<Suspense fallback={<p>Loading related products...</p>}>
<RelatedProducts category="electronics" />
</Suspense>
</div>
Šis požiūris reiškia, kad vartotojai gali matyti pagrindinę produkto informaciją, net jei susiję produktai vis dar įkeliami.
3. Serverio pusės atvaizdavimas (SSR) ir srautinis HTML
Naujos „React 18“ srautinio SSR API (renderToPipeableStream
) visiškai integruojasi su „Suspense“. Tai leidžia jūsų serveriui siųsti HTML, kai tik jis yra paruoštas, net jei kai kurios puslapio dalys (pvz., nuo duomenų priklausomi komponentai) vis dar įkeliamos. Serveris gali perduoti srautu vietos rezervavimo ženklą (iš „Suspense“ atsarginio turinio), o vėliau perduoti srautu tikrąjį turinį, kai duomenys išsisprendžia, nereikalaujant pilno kliento pusės pakartotinio atvaizdavimo. Tai žymiai pagerina suvokiamą įkėlimo našumą globaliems vartotojams esant įvairioms tinklo sąlygoms.
4. Laipsniškas pritaikymas
Jums nereikia perrašyti visos savo programos, kad galėtumėte naudoti „Suspense“. Galite jį įdiegti laipsniškai, pradedant nuo naujų funkcijų ar komponentų, kurie labiausiai pasinaudotų jo deklaratyviais įkėlimo modeliais.
5. Įrankiai ir derinimas
Nors „Suspense“ supaprastina komponentų logiką, derinimas gali būti kitoks. „React DevTools“ suteikia įžvalgų apie „Suspense“ ribas ir jų būsenas. Susipažinkite, kaip jūsų pasirinkta duomenų gavimo biblioteka atskleidžia savo vidinę būseną (pvz., „React Query Devtools“).
6. „Suspense“ atsarginio turinio laiko limitai
Labai ilgiems įkėlimo laikams galbūt norėsite įvesti laiko limitą savo „Suspense“ atsarginiam turiniui arba po tam tikro vėlavimo pereiti prie detalesnio įkėlimo indikatoriaus. „React 18“ „hook'ai“ useDeferredValue
ir useTransition
gali padėti valdyti šias subtilesnes įkėlimo būsenas, leidžiant parodyti „seną“ UI versiją, kol gaunami nauji duomenys, arba atidėti neskubius atnaujinimus.
Duomenų gavimo ateitis „React“: „React Server Components“ ir toliau
Duomenų gavimo kelionė „React“ nesibaigia su kliento pusės „Suspense“. „React Server Components“ (RSC) yra reikšminga evoliucija, žadanti ištrinti ribas tarp kliento ir serverio ir dar labiau optimizuoti duomenų gavimą.
- React Server Components (RSC): Šie komponentai atvaizduojami serveryje, tiesiogiai gauna savo duomenis, o tada siunčia tik būtiną HTML ir kliento pusės „JavaScript“ į naršyklę. Tai pašalina kliento pusės krioklius, sumažina paketų dydžius ir pagerina pradinį įkėlimo našumą. RSC veikia ranka rankon su „Suspense“: serverio komponentai gali sustabdyti vykdymą, jei jų duomenys neparuošti, o serveris gali perduoti srautu „Suspense“ atsarginį turinį klientui, kuris vėliau pakeičiamas, kai duomenys išsisprendžia. Tai keičia žaidimo taisykles programoms su sudėtingais duomenų reikalavimais, siūlant sklandžią ir labai našią patirtį, ypač naudingą vartotojams skirtinguose geografiniuose regionuose su skirtingu vėlavimu.
- Vieningas duomenų gavimas: Ilgalaikė „React“ vizija apima vieningą požiūrį į duomenų gavimą, kai pagrindinė sistema ar glaudžiai integruoti sprendimai teikia aukščiausios klasės palaikymą duomenų įkėlimui tiek serveryje, tiek kliente, viską koordinuojant „Suspense“.
- Nuolatinė bibliotekų evoliucija: Duomenų gavimo bibliotekos toliau evoliucionuos, siūlydamos dar sudėtingesnes funkcijas podėliavimui, invalidavimui ir realaus laiko atnaujinimams, remdamosi pagrindinėmis „Suspense“ galimybėmis.
„React“ toliau bręstant, „Suspense“ taps vis svarbesne galvosūkio dalimi kuriant labai našias, vartotojui draugiškas ir prižiūrimas programas. Tai skatina kūrėjus pereiti prie deklaratyvesnio ir atsparesnio asinchroninių operacijų tvarkymo būdo, perkeliant sudėtingumą iš atskirų komponentų į gerai valdomą duomenų sluoksnį.
Išvada
„React Suspense“, iš pradžių buvusi funkcija kodo padalijimui, išaugo į transformuojantį įrankį duomenų gavimui. Priimdami „Gauti-atvaizduojant“ modelį ir pasinaudodami „Suspense“ palaikančiomis bibliotekomis, kūrėjai gali žymiai pagerinti savo programų vartotojo patirtį, pašalindami įkėlimo krioklius, supaprastindami komponentų logiką ir teikdami sklandžias, koordinuotas įkėlimo būsenas. Kartu su klaidų ribomis tvirtam klaidų valdymui ir ateities pažadu – „React Server Components“, „Suspense“ suteikia mums galią kurti programas, kurios yra ne tik našios ir atsparios, bet ir iš prigimties malonesnės vartotojams visame pasaulyje. Perėjimas prie „Suspense“ pagrįstos duomenų gavimo paradigmos reikalauja konceptualaus prisitaikymo, tačiau nauda kodo aiškumo, našumo ir vartotojų pasitenkinimo požiūriu yra didelė ir verta investicijų.