Udforsk React Suspense til datahentning ud over code splitting. Forstå Fetch-As-You-Render, fejlhåndtering og fremtidssikrede mønstre for globale applikationer.
React Suspense Resource Loading: Mestring af moderne mønstre for datahentning
I den dynamiske verden af webudvikling er brugeroplevelsen (UX) altafgørende. Applikationer forventes at være hurtige, responsive og behagelige, uanset netværksforhold eller enhedskapacitet. For React-udviklere betyder dette ofte indviklet state management, komplekse indlæsningsindikatorer og en konstant kamp mod "data fetching waterfalls". Her kommer React Suspense ind i billedet – en kraftfuld, omend ofte misforstået, funktion designet til fundamentalt at transformere, hvordan vi håndterer asynkrone operationer, især datahentning.
Oprindeligt introduceret til code splitting med React.lazy()
, ligger Suspense's sande potentiale i dets evne til at orkestrere indlæsningen af *enhver* asynkron ressource, herunder data fra et API. Denne omfattende guide vil dykke dybt ned i React Suspense for ressourceindlæsning, udforske dets kernekoncepter, grundlæggende mønstre for datahentning og praktiske overvejelser for at bygge performante og robuste globale applikationer.
Udviklingen af datahentning i React: Fra imperativ til deklarativ
I mange år har datahentning i React-komponenter primært baseret sig på et almindeligt mønster: at bruge useEffect
-hooket til at starte et API-kald, administrere indlæsnings- og fejltilstande med useState
og betinget rendere baseret på disse tilstande. Selvom det er funktionelt, førte denne tilgang ofte til flere udfordringer:
- Spredning af indlæsningstilstande: Næsten hver komponent, der krævede data, havde brug for sine egne
isLoading
,isError
ogdata
-tilstande, hvilket førte til repetitiv boilerplate-kode. - Waterfalls og Race Conditions: Næstede komponenter, der hentede data, resulterede ofte i sekventielle anmodninger (waterfalls), hvor en forældrekomponent hentede data, derefter renderede, hvorefter en børnekomponent hentede sine data, og så videre. Dette øgede den samlede indlæsningstid. Race conditions kunne også opstå, når flere anmodninger blev igangsat, og svarene ankom i uorden.
- Kompleks fejlhåndtering: At distribuere fejlmeddelelser og genoprettelseslogik på tværs af adskillige komponenter kunne være besværligt og kræve prop drilling eller globale state management-løsninger.
- Ubehagelig brugeroplevelse: Flere spinnere, der dukkede op og forsvandt, eller pludselige indholdsændringer (layout shifts), kunne skabe en forstyrrende oplevelse for brugerne.
- Prop Drilling for data og tilstande: At videregive hentede data og relaterede indlæsnings-/fejltilstande ned gennem flere niveauer af komponenter blev en almindelig kilde til kompleksitet.
Overvej et typisk scenarie for datahentning uden 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-fejl! status: ${response.status}`);
}
const data = await response.json();
setUser(data);
} catch (e) {
setError(e);
} finally {
setIsLoading(false);
}
};
fetchUser();
}, [userId]);
if (isLoading) {
return <p>Indlæser brugerprofil...</p>;
}
if (error) {
return <p style={"color: red;"}>Fejl: {error.message}</p>;
}
if (!user) {
return <p>Ingen brugerdata tilgængelige.</p>;
}
return (
<div>
<h2>Bruger: {user.name}</h2>
<p>Email: {user.email}</p>
<!-- Flere brugeroplysninger -->
</div>
);
}
function App() {
return (
<div>
<h1>Velkommen til applikationen</h1>
<UserProfile userId={"123"} />
</div>
);
}
Dette mønster er allestedsnærværende, men det tvinger komponenten til at administrere sin egen asynkrone tilstand, hvilket ofte fører til et tæt koblet forhold mellem UI'et og logikken for datahentning. Suspense tilbyder et mere deklarativt og strømlinet alternativ.
Forståelse af React Suspense ud over Code Splitting
De fleste udviklere støder første gang på Suspense gennem React.lazy()
til code splitting, hvor det giver dig mulighed for at udskyde indlæsningen af en komponents kode, indtil den er nødvendig. For eksempel:
import React, { Suspense, lazy } from 'react';
const LazyComponent = lazy(() => import('./MyHeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Indlæser komponent...</div>}>
<LazyComponent />
</Suspense>
);
}
I dette scenarie, hvis MyHeavyComponent
endnu ikke er indlæst, vil <Suspense>
-grænsen opfange det promise, der kastes af lazy()
, og vise fallback
, indtil komponentens kode er klar. Den vigtigste indsigt her er, at Suspense virker ved at opfange promises, der kastes under rendering.
Denne mekanisme er ikke eksklusiv for kodeindlæsning. Enhver funktion, der kaldes under rendering, og som kaster et promise (f.eks. fordi en ressource endnu ikke er tilgængelig), kan opfanges af en Suspense-grænse højere oppe i komponenttræet. Når promis'et resolver, forsøger React at gen-rendere komponenten, og hvis ressourcen nu er tilgængelig, skjules fallback'et, og det faktiske indhold vises.
Kernekoncepter i Suspense til datahentning
For at udnytte Suspense til datahentning skal vi forstå et par kerneprincipper:
1. At kaste et Promise
I modsætning til traditionel asynkron kode, der bruger async/await
til at resolve promises, er Suspense afhængig af en funktion, der *kaster* et promise, hvis dataene ikke er klar. Når React forsøger at rendere en komponent, der kalder en sådan funktion, og dataene stadig er afventende, kastes promis'et. React 'pauser' derefter renderingen af den komponent og dens børn og leder efter den nærmeste <Suspense>
-grænse.
2. Suspense-grænsen
<Suspense>
-komponenten fungerer som en fejlgrænse for promises. Den tager en fallback
-prop, som er det UI, der skal renderes, mens nogen af dens børn (eller deres efterkommere) suspenderer (dvs. kaster et promise). Når alle promises, der er kastet inden for dens undertræ, er resolved, erstattes fallback'et af det faktiske indhold.
En enkelt Suspense-grænse kan håndtere flere asynkrone operationer. For eksempel, hvis du har to komponenter inden for den samme <Suspense>
-grænse, og hver skal hente data, vil fallback'et blive vist, indtil *begge* datahentninger er fuldført. Dette undgår at vise et delvist UI og giver en mere koordineret indlæsningsoplevelse.
3. Cache/Resource Manager (Brugerens ansvar)
Det er afgørende, at Suspense i sig selv ikke håndterer datahentning eller caching. Det er blot en koordineringsmekanisme. For at få Suspense til at fungere til datahentning har du brug for et lag, der:
- Igangsætter datahentningen.
- Cacher resultatet (resolved data eller afventende promise).
- Tilbyder en synkron
read()
-metode, der enten returnerer de cachede data med det samme (hvis tilgængelige) eller kaster det afventende promise (hvis ikke).
Denne 'resource manager' implementeres typisk ved hjælp af en simpel cache (f.eks. et Map eller et objekt) til at gemme tilstanden for hver ressource (afventende, resolved eller fejlet). Selvom du kan bygge dette manuelt til demonstrationsformål, ville du i en rigtig applikation bruge et robust datahentningsbibliotek, der integrerer med Suspense.
4. Concurrent Mode (React 18's forbedringer)
Selvom Suspense kan bruges i ældre versioner af React, frigøres dets fulde potentiale med Concurrent React (aktiveret som standard i React 18 med createRoot
). Concurrent Mode giver React mulighed for at afbryde, pause og genoptage renderingsarbejde. Dette betyder:
- Ikke-blokerende UI-opdateringer: Når Suspense viser et fallback, kan React fortsætte med at rendere andre dele af UI'et, der ikke er suspenderet, eller endda forberede det nye UI i baggrunden uden at blokere hovedtråden.
- Transitions: Nye API'er som
useTransition
giver dig mulighed for at markere visse opdateringer som 'transitions', som React kan afbryde og gøre mindre presserende, hvilket giver glattere UI-ændringer under datahentning.
Mønstre for datahentning med Suspense
Lad os udforske udviklingen af mønstre for datahentning med introduktionen af Suspense.
Mønster 1: Fetch-Then-Render (Traditionelt med Suspense-indpakning)
Dette er den klassiske tilgang, hvor data hentes, og først derefter renderes komponenten. Selvom det ikke udnytter 'throw promise'-mekanismen direkte for data, kan du indpakke en komponent, der *til sidst* render data, i en Suspense-grænse for at give et fallback. Dette handler mere om at bruge Suspense som en generisk orkestrator for indlæsnings-UI for komponenter, der til sidst bliver klar, selvom deres interne datahentning stadig er baseret på traditionel 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>Indlæser brugeroplysninger...</p>;
}
return (
<div>
<h3>Bruger: {user.name}</h3>
<p>Email: {user.email}</p>
</div>
);
}
function App() {
return (
<div>
<h1>Fetch-Then-Render Eksempel</h1>
<Suspense fallback={<div>Samlet side indlæses...</div>}>
<UserDetails userId={"1"} />
</Suspense>
</div>
);
}
Fordele: Simpelt at forstå, bagudkompatibelt. Kan bruges som en hurtig måde at tilføje en global indlæsningstilstand.
Ulemper: Fjerner ikke boilerplate inde i UserDetails
. Stadig tilbøjelig til waterfalls, hvis komponenter henter data sekventielt. Udnytter ikke fuldt ud Suspense's 'throw-and-catch'-mekanisme for selve dataene.
Mønster 2: Render-Then-Fetch (Hentning inde i render, ikke til produktion)
Dette mønster er primært for at illustrere, hvad man ikke skal gøre med Suspense direkte, da det kan føre til uendelige loops eller ydeevneproblemer, hvis det ikke håndteres omhyggeligt. Det indebærer at forsøge at hente data eller kalde en suspenderende funktion direkte i en komponents render-fase, *uden* en ordentlig caching-mekanisme.
// BRUG IKKE DETTE I PRODUKTION UDEN ET ORDENTLIGT CACHING-LAG
// Dette er udelukkende til illustration af, hvordan et direkte 'throw' konceptuelt kunne fungere.
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; // Her træder Suspense i kraft
}
function UserDetailsBadExample({ userId }) {
const user = fetchDataSynchronously(`/api/users/${userId}`);
return (
<div>
<h3>Bruger: {user.name}</h3>
<p>Email: {user.email}</p>
</div>
);
}
function App() {
return (
<div>
<h1>Render-Then-Fetch (Illustrativt, IKKE anbefalet direkte)</h1>
<Suspense fallback={<div>Indlæser bruger...</div>}>
<UserDetailsBadExample userId={"2"} />
</Suspense>
</div>
);
}
Fordele: Viser, hvordan en komponent direkte kan 'bede' om data og suspendere, hvis de ikke er klar.
Ulemper: Meget problematisk for produktion. Dette manuelle, globale fetchedData
og dataPromise
-system er simpelt, håndterer ikke flere anmodninger, invalidering eller fejltilstande robust. Det er en primitiv illustration af 'throw-a-promise'-konceptet, ikke et mønster man skal adoptere.
Mønster 3: Fetch-As-You-Render (Det ideelle Suspense-mønster)
Dette er det paradigmeskift, som Suspense virkelig muliggør for datahentning. I stedet for at vente på, at en komponent render, før dens data hentes, eller at hente alle data på forhånd, betyder Fetch-As-You-Render, at du begynder at hente data *så tidligt som muligt*, ofte *før* eller *samtidig med* renderingsprocessen. Komponenter 'læser' derefter dataene fra en cache, og hvis dataene ikke er klar, suspenderer de. Kerneideen er at adskille logikken for datahentning fra komponentens renderingslogik.
For at implementere Fetch-As-You-Render har du brug for en mekanisme til at:
- Igangsætte en datahentning uden for komponentens render-funktion (f.eks. når en rute tilgås, eller en knap klikkes).
- Gemme promis'et eller de resolved data i en cache.
- Tilbyde en måde for komponenter at 'læse' fra denne cache. Hvis dataene endnu ikke er tilgængelige, kaster læsefunktionen det afventende promise.
Dette mønster løser waterfall-problemet. Hvis to forskellige komponenter har brug for data, kan deres anmodninger igangsættes parallelt, og UI'et vil først blive vist, når *begge* er klar, orkestreret af en enkelt Suspense-grænse.
Manuel implementering (for forståelsens skyld)
For at forstå de underliggende mekanismer, lad os skabe en forenklet manuel resource manager. I en rigtig applikation ville du bruge et dedikeret bibliotek.
import React, { Suspense } from 'react';
// --- Simpel Cache/Resource Manager --- //
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);
}
// --- Datahentningsfunktioner --- //
const fetchUserById = (id) => {
console.log(`Henter bruger ${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(`Henter opslag for bruger ${userId}...`);
return new Promise(resolve => setTimeout(() => {
const posts = {
'1': [{ id: 'p1', title: 'Mit første opslag' }, { id: 'p2', title: 'Rejseeventyr' }],
'2': [{ id: 'p3', title: 'Indsigt i kodning' }],
'3': [{ id: 'p4', title: 'Globale trends' }, { id: 'p5', title: 'Lokalt køkken' }]
};
resolve(posts[userId] || []);
}, 2000));
};
// --- Komponenter --- //
function UserProfile({ userId }) {
const userResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
const user = userResource.read(); // Dette vil suspendere, hvis brugerdata ikke er klar
return (
<div>
<h3>Bruger: {user.name}</h3>
<p>Email: {user.email}</p>
</div>
);
}
function UserPosts({ userId }) {
const postsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
const posts = postsResource.read(); // Dette vil suspendere, hvis opslagsdata ikke er klar
return (
<div>
<h4>Opslag af {userId}:</h4>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
{posts.length === 0 && <li>Ingen opslag fundet.</li>}
</ul>
</div>
);
}
// --- Applikation --- //
let initialUserResource = null;
let initialPostsResource = null;
function prefetchDataForUser(userId) {
initialUserResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
initialPostsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
}
// Forhåndshent data, før App-komponenten overhovedet renderes
prefetchDataForUser('1');
function App() {
return (
<div>
<h1>Fetch-As-You-Render med Suspense</h1>
<p>Dette demonstrerer, hvordan datahentning kan ske parallelt, koordineret af Suspense.</p>
<Suspense fallback={<div>Indlæser brugerprofil og opslag...</div>}>
<UserProfile userId={"1"} />
<UserPosts userId={"1"} />
</Suspense>
<h2>En anden sektion</h2>
<Suspense fallback={<div>Indlæser en anden bruger...</div>}>
<UserProfile userId={"2"} />
</Suspense>
</div>
);
}
I dette eksempel:
- Funktionerne
createResource
ogfetchData
opretter en grundlæggende caching-mekanisme. - Når
UserProfile
ellerUserPosts
kalderresource.read()
, får de enten dataene med det samme, eller promis'et kastes. - Den nærmeste
<Suspense>
-grænse opfanger promis'et (eller promis'erne) og viser sit fallback. - Afgørende er, at vi kan kalde
prefetchDataForUser('1')
*før*App
-komponenten renderes, hvilket tillader datahentning at starte endnu tidligere.
Biblioteker til Fetch-As-You-Render
At bygge og vedligeholde en robust resource manager manuelt er komplekst. Heldigvis har flere modne datahentningsbiblioteker adopteret eller er i gang med at adoptere Suspense og leverer gennemtestede løsninger:
- React Query (TanStack Query): Tilbyder et kraftfuldt datahentnings- og caching-lag med Suspense-understøttelse. Det giver hooks som
useQuery
, der kan suspendere. Det er fremragende til REST API'er. - SWR (Stale-While-Revalidate): Et andet populært og letvægts datahentningsbibliotek, der fuldt ud understøtter Suspense. Ideelt til REST API'er, det fokuserer på at levere data hurtigt (stale) og derefter genvalidere dem i baggrunden.
- Apollo Client: En omfattende GraphQL-klient, der har robust Suspense-integration for GraphQL-queries og -mutationer.
- Relay: Facebooks egen GraphQL-klient, designet fra bunden til Suspense og Concurrent React. Det kræver et specifikt GraphQL-skema og et kompileringsstep, men tilbyder uovertruffen ydeevne og datakonsistens.
- Urql: En letvægts og meget tilpasningsdygtig GraphQL-klient med Suspense-understøttelse.
Disse biblioteker abstraherer kompleksiteten ved at oprette og administrere ressourcer, håndtere caching, genvalidering, optimistiske opdateringer og fejlhåndtering, hvilket gør det meget lettere at implementere Fetch-As-You-Render.
Mønster 4: Forhåndshentning med Suspense-bevidste biblioteker
Forhåndshentning (prefetching) er en kraftfuld optimering, hvor du proaktivt henter data, som en bruger sandsynligvis får brug for i den nærmeste fremtid, før de overhovedet anmoder om dem eksplicit. Dette kan drastisk forbedre den opfattede ydeevne.
Med Suspense-bevidste biblioteker bliver forhåndshentning problemfrit. Du kan udløse datahentninger ved brugerinteraktioner, der ikke umiddelbart ændrer UI'et, såsom at holde musen over et link eller en knap.
import React, { Suspense } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
// Antag, at disse er dine API-kald
const fetchProductById = async (id) => {
console.log(`Henter produkt ${id}...`);
return new Promise(resolve => setTimeout(() => {
const products = {
'A001': { id: 'A001', name: 'Global Widget X', price: 29.99, description: 'En alsidig widget til international brug.' },
'B002': { id: 'B002', name: 'Universal Gadget Y', price: 149.99, description: 'Banebrydende gadget, elsket verden over.' },
};
resolve(products[id]);
}, 1000));
};
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true, // Aktivér Suspense for alle queries som standard
},
},
});
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>Pris: ${product.price.toFixed(2)}</p>
<p>{product.description}</p>
</div>
);
}
function ProductList() {
const handleProductHover = (productId) => {
// Forhåndshent data, når en bruger holder musen over et produktlink
queryClient.prefetchQuery({
queryKey: ['product', productId],
queryFn: () => fetchProductById(productId),
});
console.log(`Forhåndshenter produkt ${productId}`);
};
return (
<div>
<h2>Tilgængelige produkter:</h2>
<ul>
<li>
<a href="#" onMouseEnter={() => handleProductHover('A001')}
onClick={(e) => { e.preventDefault(); /* Naviger eller vis detaljer */ }}
>Global Widget X (A001)</a>
</li>
<li>
<a href="#" onMouseEnter={() => handleProductHover('B002')}
onClick={(e) => { e.preventDefault(); /* Naviger eller vis detaljer */ }}
>Universal Gadget Y (B002)</a>
</li>
</ul>
<p>Hold musen over et produktlink for at se forhåndshentning i aktion. Åbn netværksfanen for at observere.</p>
</div>
);
}
function App() {
const [showProductA, setShowProductA] = React.useState(false);
const [showProductB, setShowProductB] = React.useState(false);
return (
<QueryClientProvider client={queryClient}>
<h1>Forhåndshentning med React Suspense (React Query)</h1>
<ProductList />
<button onClick={() => setShowProductA(true)}>Vis Global Widget X</button>
<button onClick={() => setShowProductB(true)}>Vis Universal Gadget Y</button>
{showProductA && (
<Suspense fallback={<p>Indlæser Global Widget X...</p>}>
<ProductDetails productId="A001" />
</Suspense>
)}
{showProductB && (
<Suspense fallback={<p>Indlæser Universal Gadget Y...</p>}>
<ProductDetails productId="B002" />
</Suspense>
)}
</QueryClientProvider>
);
}
I dette eksempel udløser det at holde musen over et produktlink `queryClient.prefetchQuery`, som igangsætter datahentningen i baggrunden. Hvis brugeren derefter klikker på knappen for at vise produktdetaljerne, og dataene allerede er i cachen fra forhåndshentningen, vil komponenten rendere øjeblikkeligt uden at suspendere. Hvis forhåndshentningen stadig er i gang eller ikke blev igangsat, vil Suspense vise fallback'et, indtil dataene er klar.
Fejlhåndtering med Suspense og Error Boundaries
Mens Suspense håndterer 'indlæsnings'-tilstanden ved at vise et fallback, håndterer den ikke direkte 'fejl'-tilstande. Hvis et promise, der kastes af en suspenderende komponent, rejectes (dvs. datahentning mislykkes), vil denne fejl forplante sig op gennem komponenttræet. For at håndtere disse fejl elegant og vise et passende UI, skal du bruge Error Boundaries.
En Error Boundary er en React-komponent, der implementerer enten componentDidCatch
eller static getDerivedStateFromError
-livscyklusmetoder. Den fanger JavaScript-fejl hvor som helst i sit børnekomponenttræ, inklusive fejl kastet af promises, som Suspense normalt ville fange, hvis de var afventende.
import React, { Suspense, useState } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
// --- Error Boundary Komponent --- //
class MyErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
// Opdater tilstand, så den næste render vil vise fallback-UI'et.
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Du kan også logge fejlen til en fejlrapporteringstjeneste
console.error("Fangede en fejl:", error, errorInfo);
}
render() {
if (this.state.hasError) {
// Du kan rendere ethvert brugerdefineret fallback-UI
return (
<div style={{"border": "2px solid red", "padding": "20px", "margin": "20px 0", "background": "#ffe0e0"}}>
<h2>Noget gik galt!</h2>
<p>{this.state.error && this.state.error.message}</p>
<p>Prøv venligst at genindlæse siden eller kontakt support.</p>
<button onClick={() => this.setState({ hasError: false, error: null })}>Prøv igen</button>
</div>
);
}
return this.props.children;
}
}
// --- Datahentning (med potentiale for fejl) --- //
const fetchItemById = async (id) => {
console.log(`Forsøger at hente element ${id}...`);
return new Promise((resolve, reject) => setTimeout(() => {
if (id === 'error-item') {
reject(new Error('Kunne ikke indlæse element: Netværk utilgængeligt eller element ikke fundet.'));
} else if (id === 'slow-item') {
resolve({ id: 'slow-item', name: 'Leveret langsomt', data: 'Dette element tog et stykke tid, men ankom!', status: 'success' });
} else {
resolve({ id, name: `Element ${id}`, data: `Data for element ${id}` });
}
}, id === 'slow-item' ? 3000 : 800));
};
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true,
retry: false, // For demonstration, deaktiver retry, så fejlen er øjeblikkelig
},
},
});
function DisplayItem({ itemId }) {
const { data: item } = useQuery({
queryKey: ['item', itemId],
queryFn: () => fetchItemById(itemId),
});
return (
<div>
<h3>Elementdetaljer:</h3>
<p>ID: {item.id}</p>
<p>Navn: {item.name}</p>
<p>Data: {item.data}</p>
</div>
);
}
function App() {
const [fetchType, setFetchType] = useState('normal-item');
return (
<QueryClientProvider client={queryClient}>
<h1>Suspense og Error Boundaries</h1>
<div>
<button onClick={() => setFetchType('normal-item')}>Hent normalt element</button>
<button onClick={() => setFetchType('slow-item')}>Hent langsomt element</button>
<button onClick={() => setFetchType('error-item')}>Hent fejlelement</button>
</div>
<MyErrorBoundary>
<Suspense fallback={<p>Indlæser element via Suspense...</p>}>
<DisplayItem itemId={fetchType} />
</Suspense>
</MyErrorBoundary>
</QueryClientProvider>
);
}
Ved at indpakke din Suspense-grænse (eller de komponenter, der kan suspendere) med en Error Boundary sikrer du, at netværksfejl eller serverfejl under datahentning fanges og håndteres elegant, hvilket forhindrer hele applikationen i at crashe. Dette giver en robust og brugervenlig oplevelse, der giver brugerne mulighed for at forstå problemet og potentielt prøve igen.
State Management og datainvalidering med Suspense
Det er vigtigt at præcisere, at React Suspense primært adresserer den indledende indlæsningstilstand for asynkrone ressourcer. Det håndterer ikke i sig selv klientsidens cache, datainvalidering eller orkestrering af mutationer (opret, opdater, slet-operationer) og deres efterfølgende UI-opdateringer.
Det er her, de Suspense-bevidste datahentningsbiblioteker (React Query, SWR, Apollo Client, Relay) bliver uundværlige. De supplerer Suspense ved at levere:
- Robust Caching: De vedligeholder en sofistikeret in-memory cache af hentede data, serverer dem øjeblikkeligt, hvis de er tilgængelige, og håndterer baggrunds-genvalidering.
- Datainvalidering og genhentning: De tilbyder mekanismer til at markere cachede data som 'stale' og genhente dem (f.eks. efter en mutation, en brugerinteraktion eller ved vinduesfokus).
- Optimistiske opdateringer: For mutationer giver de dig mulighed for at opdatere UI'et øjeblikkeligt (optimistisk) baseret på det forventede resultat af et API-kald og derefter rulle tilbage, hvis det faktiske API-kald mislykkes.
- Global tilstandssynkronisering: De sikrer, at hvis data ændres fra en del af din applikation, opdateres alle komponenter, der viser disse data, automatisk.
- Indlæsnings- og fejltilstande for mutationer: Mens
useQuery
kan suspendere, levereruseMutation
typiskisLoading
ogisError
-tilstande for selve mutationsprocessen, da mutationer ofte er interaktive og kræver øjeblikkelig feedback.
Uden et robust datahentningsbibliotek ville implementering af disse funktioner oven på en manuel Suspense resource manager være en betydelig opgave, der i det væsentlige kræver, at du bygger dit eget datahentnings-framework.
Praktiske overvejelser og bedste praksis
At adoptere Suspense til datahentning er en betydelig arkitektonisk beslutning. Her er nogle praktiske overvejelser for en global applikation:
1. Ikke alle data har brug for Suspense
Suspense er ideelt til kritiske data, der direkte påvirker den indledende rendering af en komponent. For ikke-kritiske data, baggrundshentninger eller data, der kan indlæses lazy uden en stærk visuel påvirkning, kan traditionel useEffect
eller pre-rendering stadig være passende. Overdreven brug af Suspense kan føre til en mindre granulær indlæsningsoplevelse, da en enkelt Suspense-grænse venter på, at *alle* dens børn er resolved.
2. Granularitet af Suspense-grænser
Placer dine <Suspense>
-grænser med omtanke. En enkelt, stor grænse øverst i din applikation kan skjule hele siden bag en spinner, hvilket kan være frustrerende. Mindre, mere granulære grænser giver forskellige dele af din side mulighed for at indlæse uafhængigt, hvilket giver en mere progressiv og responsiv oplevelse. For eksempel en grænse omkring en brugerprofilkomponent og en anden omkring en liste over anbefalede produkter.
<div>
<h1>Produktside</h1>
<Suspense fallback={<p>Indlæser primære produktdetaljer...</p>}>
<ProductDetails id="prod123" />
</Suspense>
<hr />
<h2>Relaterede produkter</h2>
<Suspense fallback={<p>Indlæser relaterede produkter...</p>}>
<RelatedProducts category="electronics" />
</Suspense>
</div>
Denne tilgang betyder, at brugere kan se de primære produktdetaljer, selvom de relaterede produkter stadig indlæses.
3. Server-Side Rendering (SSR) og Streaming HTML
React 18's nye streaming SSR API'er (renderToPipeableStream
) er fuldt integreret med Suspense. Dette giver din server mulighed for at sende HTML, så snart den er klar, selvom dele af siden (som dataafhængige komponenter) stadig indlæses. Serveren kan streame en pladsholder (fra Suspense fallback'et) og derefter streame det faktiske indhold, når dataene er resolved, uden at kræve en fuld gen-rendering på klientsiden. Dette forbedrer markant den opfattede indlæsningsydelse for globale brugere under forskellige netværksforhold.
4. Inkrementel adoption
Du behøver ikke at omskrive hele din applikation for at bruge Suspense. Du kan introducere det gradvist, startende med nye funktioner eller komponenter, der ville have mest gavn af dets deklarative indlæsningsmønstre.
5. Værktøjer og debugging
Mens Suspense forenkler komponentlogik, kan debugging være anderledes. React DevTools giver indsigt i Suspense-grænser og deres tilstande. Gør dig bekendt med, hvordan dit valgte datahentningsbibliotek eksponerer sin interne tilstand (f.eks. React Query Devtools).
6. Timeouts for Suspense Fallbacks
For meget lange indlæsningstider kan du overveje at introducere en timeout til dit Suspense fallback, eller skifte til en mere detaljeret indlæsningsindikator efter en vis forsinkelse. useDeferredValue
og useTransition
-hooks i React 18 kan hjælpe med at håndtere disse mere nuancerede indlæsningstilstande, hvilket giver dig mulighed for at vise en 'gammel' version af UI'et, mens nye data hentes, eller udsætte ikke-presserende opdateringer.
Fremtiden for datahentning i React: React Server Components og videre
Rejsen for datahentning i React stopper ikke med klientside-Suspense. React Server Components (RSC) repræsenterer en betydelig udvikling, der lover at udviske grænserne mellem klient og server og yderligere optimere datahentning.
- React Server Components (RSC): Disse komponenter renderes på serveren, henter deres data direkte og sender derefter kun den nødvendige HTML og klientside-JavaScript til browseren. Dette eliminerer klientside-waterfalls, reducerer bundle-størrelser og forbedrer den indledende indlæsningsydelse. RSC'er arbejder hånd i hånd med Suspense: serverkomponenter kan suspendere, hvis deres data ikke er klar, og serveren kan streame et Suspense fallback ned til klienten, som derefter erstattes, når dataene er resolved. Dette er en game-changer for applikationer med komplekse datakrav, der tilbyder en problemfri og yderst performant oplevelse, især gavnlig for brugere på tværs af forskellige geografiske regioner med varierende latenstid.
- Samlet datahentning: Den langsigtede vision for React involverer en samlet tilgang til datahentning, hvor kernen i frameworket eller tæt integrerede løsninger giver førsteklasses support til indlæsning af data både på serveren og klienten, alt sammen orkestreret af Suspense.
- Fortsat biblioteksudvikling: Datahentningsbiblioteker vil fortsætte med at udvikle sig og tilbyde endnu mere sofistikerede funktioner til caching, invalidering og realtidsopdateringer, bygget oven på de grundlæggende kapaciteter i Suspense.
I takt med at React fortsætter med at modnes, vil Suspense være en stadig mere central brik i puslespillet for at bygge yderst performante, brugervenlige og vedligeholdelsesvenlige applikationer. Det skubber udviklere mod en mere deklarativ og robust måde at håndtere asynkrone operationer på, og flytter kompleksiteten fra individuelle komponenter til et veladministreret datalag.
Konklusion
React Suspense, oprindeligt en funktion til code splitting, er blomstret op til at blive et transformativt værktøj til datahentning. Ved at omfavne Fetch-As-You-Render-mønsteret og udnytte Suspense-bevidste biblioteker kan udviklere markant forbedre brugeroplevelsen af deres applikationer, eliminere indlæsnings-waterfalls, forenkle komponentlogik og levere glatte, koordinerede indlæsningstilstande. Kombineret med Error Boundaries for robust fejlhåndtering og det fremtidige løfte om React Server Components, giver Suspense os mulighed for at bygge applikationer, der ikke kun er performante og robuste, men også i sagens natur mere behagelige for brugere over hele kloden. Skiftet til et Suspense-drevet paradigme for datahentning kræver en konceptuel justering, men fordelene i form af kodeklarhed, ydeevne og brugertilfredshed er betydelige og investeringen værd.