Įvaldykite React Suspense duomenų gavimui. Išmokite deklaratyviai valdyti įkėlimo būsenas, gerinti UX su perėjimais ir apdoroti klaidas su Error Boundaries.
React Suspense ribos: išsami deklaratyvaus įkėlimo būsenų valdymo analizė
Šiuolaikinio žiniatinklio kūrimo pasaulyje nepriekaištingos ir greitai reaguojančios vartotojo patirties sukūrimas yra svarbiausias tikslas. Vienas iš nuolatinių iššūkių, su kuriais susiduria programuotojai, yra įkėlimo būsenų valdymas. Nuo duomenų gavimo vartotojo profiliui iki naujos programos skilties įkėlimo – laukimo akimirkos yra kritinės. Istoriškai tai buvo susiję su sudėtingu loginių (boolean) žymų, tokių kaip isLoading
, isFetching
ir hasError
, tinklu, išmėtytu po visus mūsų komponentus. Šis imperatyvus požiūris apkrauna mūsų kodą, komplikuoja logiką ir yra dažnas klaidų, tokių kaip lenktynių sąlygos (race conditions), šaltinis.
Pristatome „React Suspense“. Iš pradžių pristatytas kodo skaidymui su React.lazy()
, jo galimybės su „React 18“ smarkiai išsiplėtė ir tapo galingu, aukščiausio lygio mechanizmu asinchroninėms operacijoms, ypač duomenų gavimui, valdyti. „Suspense“ leidžia mums valdyti įkėlimo būsenas deklaratyviai, iš esmės keičiant tai, kaip rašome ir mąstome apie savo komponentus. Užuot klausę „Ar aš įkeliu?“, mūsų komponentai gali tiesiog pasakyti: „Man reikia šių duomenų, kad galėčiau atvaizduoti. Kol laukiu, prašau, parodykite šį atsarginį UI.“
Šis išsamus vadovas nuves jus į kelionę nuo tradicinių būsenos valdymo metodų iki deklaratyvios „React Suspense“ paradigmos. Mes išnagrinėsime, kas yra „Suspense“ ribos, kaip jos veikia tiek kodo skaidymui, tiek duomenų gavimui, ir kaip organizuoti sudėtingus įkėlimo UI, kurie džiugintų jūsų vartotojus, o ne juos vargintų.
Senasis būdas: varginantis rankinis įkėlimo būsenų valdymas
Prieš pilnai įvertinant „Suspense“ eleganciją, būtina suprasti problemą, kurią jis sprendžia. Pažvelkime į tipišką komponentą, kuris gauna duomenis naudodamas useEffect
ir useState
kabliukus (hooks).
Įsivaizduokite komponentą, kuris turi gauti ir parodyti vartotojo duomenis:
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(() => {
// Atstatome būseną naujam userId
setIsLoading(true);
setUser(null);
setError(null);
const fetchUser = async () => {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error('Tinklo atsakas nebuvo sėkmingas');
}
const data = await response.json();
setUser(data);
} catch (err) {
setError(err);
} finally {
setIsLoading(false);
}
};
fetchUser();
}, [userId]); // Iš naujo gauname duomenis, kai pasikeičia userId
if (isLoading) {
return <p>Įkeliamas profilis...</p>;
}
if (error) {
return <p>Klaida: {error.message}</p>;
}
return (
<div>
<h1>{user.name}</h1>
<p>El. paštas: {user.email}</p>
</div>
);
}
Šis modelis yra funkcionalus, tačiau turi keletą trūkumų:
- Šabloninis kodas: Mums reikia mažiausiai trijų būsenos kintamųjų (
data
,isLoading
,error
) kiekvienai asinchroninei operacijai. Tai prastai mastelio keitimo atžvilgiu sudėtingoje programoje. - Išskaidyta logika: Atvaizdavimo logika yra fragmentuota su sąlyginiais patikrinimais (
if (isLoading)
,if (error)
). Pagrindinė „laimingo atvejo“ atvaizdavimo logika nustumiama į patį apačią, todėl komponentą sunkiau skaityti. - Lenktynių sąlygos (Race Conditions):
useEffect
kabliukas reikalauja kruopštaus priklausomybių valdymo. Be tinkamo išvalymo, greitas atsakas galėtų būti perrašytas lėto atsako, jeiuserId
parametras greitai pasikeistų. Nors mūsų pavyzdys paprastas, sudėtingi scenarijai gali lengvai įvesti subtilių klaidų. - Kaskadiniai duomenų gavimai (Waterfall Fetches): Jei vaikiniam komponentui taip pat reikia gauti duomenis, jis negali net pradėti atvaizdavimo (ir, atitinkamai, duomenų gavimo), kol tėvinis komponentas nebaigs įkėlimo. Tai veda prie neefektyvių duomenų įkėlimo kaskadų.
Pristatome „React Suspense“: paradigmos pokytis
„Suspense“ apverčia šį modelį aukštyn kojomis. Užuot komponentui valdant įkėlimo būseną viduje, jis tiesiogiai praneša „React“ apie savo priklausomybę nuo asinchroninės operacijos. Jei duomenys, kurių jam reikia, dar nėra pasiekiami, komponentas „sustabdo“ atvaizdavimą.
Kai komponentas sustabdomas, „React“ kyla aukštyn komponentų medžiu, ieškodamas artimiausios „Suspense“ ribos. „Suspense“ riba yra jūsų medyje apibrėžtas komponentas, naudojant <Suspense>
. Ši riba tada atvaizduos atsarginį UI (pvz., suktuką ar karkasinį įkėlėją), kol visi jame esantys komponentai išspręs savo duomenų priklausomybes.
Pagrindinė idėja yra sujungti duomenų priklausomybę su komponentu, kuriam jos reikia, tuo pačiu centralizuojant įkėlimo UI aukštesniame komponentų medžio lygmenyje. Tai išvalo komponentų logiką ir suteikia jums galingą kontrolę vartotojo įkėlimo patirčiai.
Kaip komponentas „sustabdo“ vykdymą?
„Suspense“ magija slypi modelyje, kuris iš pradžių gali atrodyti neįprastas: „Promise“ išmetimas. Su „Suspense“ suderinamas duomenų šaltinis veikia taip:
- Kai komponentas prašo duomenų, duomenų šaltinis patikrina, ar jis turi duomenis podėlyje (cache).
- Jei duomenys yra prieinami, jis juos grąžina sinchroniškai.
- Jei duomenys nėra prieinami (t. y., jie šiuo metu gaunami), duomenų šaltinis išmeta „Promise“, kuris atspindi vykstantį gavimo prašymą.
„React“ pagauna šį išmestą „Promise“. Tai nesugadina jūsų programos. Vietoj to, jis tai interpretuoja kaip signalą: „Šis komponentas dar nėra pasirengęs atvaizduoti. Pristabdykite jį ir ieškokite virš jo esančios „Suspense“ ribos, kad parodytumėte atsarginį UI.“ Kai „Promise“ bus įvykdytas, „React“ bandys iš naujo atvaizduoti komponentą, kuris dabar gaus savo duomenis ir sėkmingai atvaizduos.
<Suspense>
riba: jūsų deklaratyvus įkėlimo UI
<Suspense>
komponentas yra šio modelio širdis. Jį naudoti neįtikėtinai paprasta, jis priima vieną privalomą parametrą: fallback
.
import { Suspense } from 'react';
function App() {
return (
<div>
<h1>Mano programa</h1>
<Suspense fallback={<p>Įkeliamas turinys...</p>}>
<SomeComponentThatFetchesData />
</Suspense>
</div>
);
}
Šiame pavyzdyje, jei SomeComponentThatFetchesData
sustabdys vykdymą, vartotojas matys pranešimą „Įkeliamas turinys...“, kol duomenys bus paruošti. Atsarginis elementas (fallback) gali būti bet koks galiojantis „React“ mazgas, nuo paprastos eilutės iki sudėtingo karkasinio komponento.
Klasikinis panaudojimo atvejis: kodo skaidymas su React.lazy()
Labiausiai įsitvirtinęs „Suspense“ panaudojimas yra kodo skaidymas. Tai leidžia atidėti komponento „JavaScript“ kodo įkėlimą, kol jis iš tikrųjų bus reikalingas.
import React, { Suspense, lazy } from 'react';
// Šio komponento kodas nebus pradiniame pakete.
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<div>
<h2>Turinys, kuris įkeliamas iš karto</h2>
<Suspense fallback={<div>Įkeliamas komponentas...</div>}>
<HeavyComponent />
</Suspense>
</div>
);
}
Čia „React“ gaus „JavaScript“ kodą HeavyComponent
komponentui tik tada, kai pirmą kartą bandys jį atvaizduoti. Kol jis bus gaunamas ir analizuojamas, bus rodomas „Suspense“ atsarginis elementas. Tai yra galinga technika, skirta pagerinti pradinio puslapio įkėlimo laiką.
Šiuolaikinė riba: duomenų gavimas su „Suspense“
Nors „React“ suteikia „Suspense“ mechanizmą, jis nepateikia konkretaus duomenų gavimo kliento. Norint naudoti „Suspense“ duomenų gavimui, jums reikia duomenų šaltinio, kuris su juo integruojasi (t. y., kuris išmeta „Promise“, kai duomenys laukiami).
Tokios sistemos kaip „Relay“ ir „Next.js“ turi integruotą, aukščiausio lygio palaikymą „Suspense“. Populiarios duomenų gavimo bibliotekos, tokios kaip „TanStack Query“ (buvusi „React Query“) ir „SWR“, taip pat siūlo eksperimentinį arba pilną palaikymą.
Kad suprastumėte koncepciją, sukurkime labai paprastą, konceptualų apvalkalą aplink fetch
API, kad jis būtų suderinamas su „Suspense“. Pastaba: tai supaprastintas pavyzdys edukaciniais tikslais ir nėra paruoštas produkcijai. Jam trūksta tinkamo podėlio valdymo ir klaidų apdorojimo subtilybių.
// data-fetcher.js
// Paprasta podėlio (cache) sistema rezultatams saugoti
const cache = new Map();
export function fetchData(url) {
if (!cache.has(url)) {
cache.set(url, { status: 'pending', promise: fetchAndCache(url) });
}
const record = cache.get(url);
if (record.status === 'pending') {
throw record.promise; // Čia ir yra magija!
}
if (record.status === 'error') {
throw record.error;
}
if (record.status === 'success') {
return record.data;
}
}
async function fetchAndCache(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Duomenų gavimas nepavyko, būsena ${response.status}`);
}
const data = await response.json();
cache.set(url, { status: 'success', data });
} catch (e) {
cache.set(url, { status: 'error', error: e });
}
}
Šis apvalkalas palaiko paprastą kiekvieno URL būseną. Kai iškviečiama fetchData
, ji patikrina būseną. Jei ji yra laukiama, ji išmeta „promise“. Jei ji sėkminga, ji grąžina duomenis. Dabar perrašykime mūsų UserProfile
komponentą, naudodami tai.
// UserProfile.js
import React, { Suspense } from 'react';
import { fetchData } from './data-fetcher';
// Komponentas, kuris realiai naudoja duomenis
function ProfileDetails({ userId }) {
// Bandoma nuskaityti duomenis. Jei jie neparuošti, vykdymas bus sustabdytas.
const user = fetchData(`https://api.example.com/users/${userId}`);
return (
<div>
<h1>{user.name}</h1>
<p>El. paštas: {user.email}</p>
</div>
);
}
// Tėvinis komponentas, kuris apibrėžia įkėlimo būsenos UI
export function UserProfile({ userId }) {
return (
<Suspense fallback={<p>Įkeliamas profilis...</p>}>
<ProfileDetails userId={userId} />
</Suspense>
);
}
Pažiūrėkite į skirtumą! ProfileDetails
komponentas yra švarus ir sutelktas tik į duomenų atvaizdavimą. Jame nėra isLoading
ar error
būsenų. Jis tiesiog prašo duomenų, kurių jam reikia. Atsakomybė rodyti įkėlimo indikatorių buvo perkelta į tėvinį komponentą UserProfile
, kuris deklaratyviai nurodo, ką rodyti laukiant.
Sudėtingų įkėlimo būsenų organizavimas
Tikroji „Suspense“ galia atsiskleidžia, kai kuriate sudėtingus UI su keliomis asinchroninėmis priklausomybėmis.
Įdėtosios „Suspense“ ribos laipsniškam UI atvaizdavimui
Galite dėti „Suspense“ ribas vieną į kitą, kad sukurtumėte tobulesnę įkėlimo patirtį. Įsivaizduokite prietaisų skydelio puslapį su šonine juosta, pagrindine turinio sritimi ir naujausių veiklų sąrašu. Kiekvienam iš jų gali prireikti savo duomenų gavimo.
function DashboardPage() {
return (
<div>
<h1>Prietaisų skydelis</h1>
<div className="layout">
<Suspense fallback={<p>Įkeliama navigacija...</p>}>
<Sidebar />
</Suspense>
<main>
<Suspense fallback={<ProfileSkeleton />}>
<MainContent />
</Suspense>
<Suspense fallback={<ActivityFeedSkeleton />}>
<ActivityFeed />
</Suspense>
</main>
</div>
</div>
);
}
Su šia struktūra:
Sidebar
gali pasirodyti, kai tik jo duomenys bus paruošti, net jei pagrindinis turinys vis dar įkeliamas.MainContent
irActivityFeed
gali įsikelti nepriklausomai. Vartotojas mato detalų karkasinį įkėlėją kiekvienai skilčiai, kuris suteikia geresnį kontekstą nei vienas bendras suktukas visam puslapiui.
Tai leidžia kuo greičiau parodyti vartotojui naudingą turinį, dramatiškai pagerinant suvokiamą našumą.
Vengiame UI „spragsėjimo“ efekto
Kartais laipsniškas požiūris gali sukelti erzinantį efektą, kai keli suktukai atsiranda ir dingsta greita seka, efektas dažnai vadinamas „spragsėjimu“ (popcorning). Norėdami tai išspręsti, galite perkelti „Suspense“ ribą aukščiau medyje.
function DashboardPage() {
return (
<div>
<h1>Prietaisų skydelis</h1>
<Suspense fallback={<DashboardSkeleton />}>
<div className="layout">
<Sidebar />
<main>
<MainContent />
<ActivityFeed />
</main>
</div>
</Suspense>
</div>
);
}
Šioje versijoje vienas DashboardSkeleton
rodomas tol, kol visi vaikiniai komponentai (Sidebar
, MainContent
, ActivityFeed
) turės paruoštus duomenis. Tada visas prietaisų skydelis pasirodo vienu metu. Pasirinkimas tarp įdėtųjų ribų ir vienos aukštesnio lygio ribos yra UX dizaino sprendimas, kurį „Suspense“ leidžia įgyvendinti trivialiai.
Klaidų apdorojimas su klaidų ribomis (Error Boundaries)
„Suspense“ valdo laukiama (pending) „promise“ būseną, bet kaip dėl atmesta (rejected) būsenos? Jei komponento išmestas „promise“ atmetamas (pvz., dėl tinklo klaidos), tai bus traktuojama kaip bet kuri kita atvaizdavimo klaida „React“ programoje.
Sprendimas yra naudoti klaidų ribas (Error Boundaries). Klaidų riba yra klasės komponentas, kuris apibrėžia specialų gyvavimo ciklo metodą, componentDidCatch()
arba statinį metodą getDerivedStateFromError()
. Jis pagauna „JavaScript“ klaidas bet kurioje savo vaikinių komponentų medžio vietoje, registruoja tas klaidas ir rodo atsarginį UI.
Štai paprastas „Error Boundary“ komponentas:
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
// Atnaujiname būseną, kad kitas atvaizdavimas parodytų atsarginį UI.
return { hasError: true, error: error };
}
componentDidCatch(error, errorInfo) {
// Taip pat galite registruoti klaidą klaidų pranešimo tarnyboje
console.error("Pagauta klaida:", error, errorInfo);
}
render() {
if (this.state.hasError) {
// Galite atvaizduoti bet kokį pasirinktinį atsarginį UI
return <h1>Kažkas nutiko negerai. Bandykite dar kartą.</h1>;
}
return this.props.children;
}
}
Tada galite derinti klaidų ribas su „Suspense“, kad sukurtumėte patikimą sistemą, kuri valdo visas tris būsenas: laukiama, sėkminga ir klaida.
import { Suspense } from 'react';
import ErrorBoundary from './ErrorBoundary';
import { UserProfile } from './UserProfile';
function App() {
return (
<div>
<h2>Vartotojo informacija</h2>
<ErrorBoundary>
<Suspense fallback={<p>Įkeliama...</p>}>
<UserProfile userId={123} />
</Suspense>
</ErrorBoundary>
</div>
);
}
Naudojant šį modelį, jei duomenų gavimas UserProfile
viduje pavyksta, rodomas profilis. Jei jis laukiamas, rodomas „Suspense“ atsarginis elementas. Jei jis nepavyksta, rodomas klaidų ribos atsarginis elementas. Logika yra deklaratyvi, kompozicinė ir lengvai suprantama.
Perėjimai (Transitions): raktas į neblokuojančius UI atnaujinimus
Yra dar viena paskutinė dėlionės dalis. Apsvarstykite vartotojo sąveiką, kuri sukelia naują duomenų gavimą, pavyzdžiui, paspaudus mygtuką „Kitas“, norint peržiūrėti kitą vartotojo profilį. Su aukščiau aprašyta konfigūracija, tą akimirką, kai paspaudžiamas mygtukas ir pasikeičia userId
parametras, UserProfile
komponentas vėl bus sustabdytas. Tai reiškia, kad šiuo metu matomas profilis išnyks ir bus pakeistas įkėlimo atsarginiu elementu. Tai gali atrodyti staiga ir trikdančiai.
Štai kur pasirodo perėjimai (transitions). Perėjimai yra nauja „React 18“ funkcija, leidžianti pažymėti tam tikrus būsenos atnaujinimus kaip neskubius. Kai būsenos atnaujinimas yra apgaubtas perėjimu, „React“ toliau rodys seną UI (pasenusį turinį), kol fone ruoš naują turinį. Jis atliks UI atnaujinimą tik tada, kai naujas turinys bus paruoštas rodymui.
Pagrindinė API tam yra useTransition
kabliukas.
import React, { useState, useTransition, Suspense } from 'react';
import { UserProfile } from './UserProfile';
function ProfileSwitcher() {
const [userId, setUserId] = useState(1);
const [isPending, startTransition] = useTransition();
const handleNextClick = () => {
startTransition(() => {
setUserId(id => id + 1);
});
};
return (
<div>
<button onClick={handleNextClick} disabled={isPending}>
Kitas vartotojas
</button>
{isPending && <span> Įkeliamas naujas profilis...</span>}
<ErrorBoundary>
<Suspense fallback={<p>Įkeliamas pradinis profilis...</p>}>
<UserProfile userId={userId} />
</Suspense>
</ErrorBoundary>
</div>
);
}
Štai kas vyksta dabar:
- Įkeliamas pradinis profilis su
userId: 1
, rodomas „Suspense“ atsarginis elementas. - Vartotojas paspaudžia „Kitas vartotojas“.
setUserId
iškvietimas yra apgaubtasstartTransition
.- „React“ pradeda atmintyje atvaizduoti
UserProfile
su naujuuserId
, lygiu 2. Tai sukelia jo sustabdymą. - Svarbiausia, užuot rodęs „Suspense“ atsarginį elementą, „React“ toliau ekrane rodo seną UI (vartotojo 1 profilį).
isPending
loginė reikšmė, grąžinta išuseTransition
, tampatrue
, leidžiant mums rodyti subtilų, įterptą įkėlimo indikatorių, neišmontuojant seno turinio.- Kai tik duomenys vartotojui 2 yra gauti ir
UserProfile
gali sėkmingai atvaizduoti, „React“ patvirtina atnaujinimą, ir naujas profilis sklandžiai pasirodo.
Perėjimai suteikia galutinį kontrolės lygį, leidžiantį kurti sudėtingas ir vartotojui draugiškas įkėlimo patirtis, kurios niekada neatrodo trikdančios.
Geriausios praktikos ir bendri aspektai
- Strategiškai išdėstykite ribas: Nevyniokite kiekvieno mažo komponento į „Suspense“ ribą. Išdėstykite jas logiškose programos vietose, kur įkėlimo būsena yra prasminga vartotojui, pavyzdžiui, puslapyje, dideliame skydelyje ar svarbiame valdiklyje.
- Kurkite prasmingus atsarginius elementus: Bendriniai suktukai yra lengvas sprendimas, tačiau karkasiniai įkėlėjai, imituojantys įkeliamo turinio formą, suteikia daug geresnę vartotojo patirtį. Jie sumažina išdėstymo poslinkį ir padeda vartotojui numatyti, koks turinys pasirodys.
- Atsižvelkite į prieinamumą: Rodydami įkėlimo būsenas, užtikrinkite, kad jos būtų prieinamos. Naudokite ARIA atributus, tokius kaip
aria-busy="true"
, turinio konteineryje, kad informuotumėte ekrano skaitytuvų vartotojus, kad turinys atnaujinamas. - Pasinaudokite serverio komponentais: „Suspense“ yra pagrindinė „React“ serverio komponentų (RSC) technologija. Naudojant sistemas kaip „Next.js“, „Suspense“ leidžia transliuoti HTML iš serverio, kai duomenys tampa prieinami, o tai lemia neįtikėtinai greitą pradinį puslapio įkėlimą pasaulinei auditorijai.
- Pasinaudokite ekosistema: Nors svarbu suprasti pagrindinius principus, produkcijos programoms pasikliaukite patikrintomis bibliotekomis, tokiomis kaip „TanStack Query“, „SWR“ ar „Relay“. Jos valdo podėlio valdymą, dublikatų šalinimą ir kitus sudėtingumus, tuo pačiu užtikrindamos sklandžią „Suspense“ integraciją.
Išvada
„React Suspense“ yra daugiau nei tik nauja funkcija; tai esminė evoliucija, kaip mes artėjame prie asinchroniškumo „React“ programose. Atsisakydami rankinių, imperatyvių įkėlimo žymų ir priimdami deklaratyvų modelį, galime rašyti komponentus, kurie yra švaresni, atsparesni ir lengviau komponuojami.
Derindami <Suspense>
laukiančioms būsenoms, klaidų ribas nesėkmės būsenoms ir useTransition
sklandiems atnaujinimams, turite pilną ir galingą įrankių rinkinį. Galite organizuoti viską nuo paprastų įkėlimo suktukų iki sudėtingų, laipsniškų prietaisų skydelio atvaizdavimų su minimaliu, nuspėjamu kodu. Pradėję integruoti „Suspense“ į savo projektus, pamatysite, kad tai ne tik pagerina jūsų programos našumą ir vartotojo patirtį, bet ir dramatiškai supaprastina jūsų būsenos valdymo logiką, leidžiant jums sutelkti dėmesį į tai, kas iš tikrųjų svarbu: kurti puikias funkcijas.