Udforsk avancerede teknikker til parallel datahentning i React ved hjælp af Suspense, hvilket forbedrer applikationsydelsen og brugeroplevelsen. Lær strategier til at koordinere flere asynkrone operationer og effektivt håndtere indlæsningstilstande.
React Suspense Koordinering: Mestring af Parallel Datahentning
React Suspense har revolutioneret, hvordan vi håndterer asynkrone operationer, især datahentning. Det giver komponenter mulighed for at "suspende" rendering, mens de venter på, at data indlæses, hvilket giver en deklarativ måde at håndtere indlæsningstilstande på. Men blot at ombryde individuelle datahentninger med Suspense kan føre til en vandfaldseffekt, hvor en hentning fuldføres, før den næste starter, hvilket negativt påvirker ydeevnen. Dette blogindlæg dykker ned i avancerede strategier til at koordinere flere datahentninger parallelt ved hjælp af Suspense, optimere din applikations responsivitet og forbedre brugeroplevelsen for et globalt publikum.
Forståelse af Vandfaldsproblemet i Datahentning
Forestil dig et scenarie, hvor du skal vise en brugerprofil med deres navn, avatar og seneste aktivitet. Hvis du henter hver datadel sekventielt, ser brugeren en indlæsningsspinner for navnet, derefter en anden for avataren og til sidst en for aktivitetsstrømmen. Dette sekventielle indlæsningsmønster skaber en vandfaldseffekt, der forsinker renderingen af den komplette profil og frustrerer brugerne. For internationale brugere med varierende netværkshastigheder kan denne forsinkelse være endnu mere udtalt.
Overvej dette forenklede kodeuddrag:
function UserProfile() {
const name = useName(); // Henter brugernavn
const avatar = useAvatar(name); // Henter avatar baseret på navn
const activity = useActivity(name); // Henter aktivitet baseret på navn
return (
<div>
<h2>{name}</h2>
<img src={avatar} alt="User Avatar" />
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
</div>
);
}
I dette eksempel er useAvatar og useActivity afhængige af resultatet af useName. Dette skaber et klart vandfald – useAvatar og useActivity kan ikke starte med at hente data, før useName er fuldført. Dette er ineffektivt og en almindelig ydelsesflaskehals.
Strategier til Parallel Datahentning med Suspense
Nøglen til optimering af datahentning med Suspense er at starte alle dataanmodninger samtidigt. Her er flere strategier, du kan anvende:
1. Forudindlæsning af Data med `React.preload` og Ressourcer
En af de mest kraftfulde teknikker er at forudindlæse data, før komponenten overhovedet renderes. Dette involverer at oprette en "ressource" (et objekt, der indkapsler datahentningsløftet) og forudhente dataene. `React.preload` hjælper med dette. Når komponenten har brug for dataene, er de allerede tilgængelige, hvilket næsten helt eliminerer indlæsningstilstanden.
Overvej en ressource til at hente et produkt:
const createProductResource = (productId) => {
let promise;
let product;
let error;
const suspender = new Promise((resolve, reject) => {
promise = fetch(`/api/products/${productId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
product = data;
resolve();
})
.catch(e => {
error = e;
reject(e);
});
});
return {
read() {
if (error) {
throw error;
}
if (product) {
return product;
}
throw suspender;
},
};
};
// Brug:
const productResource = createProductResource(123);
function ProductDetails() {
const product = productResource.read();
return (<div>{product.name}</div>);
}
Nu kan du forudindlæse denne ressource, før komponenten ProductDetails renderes. For eksempel under ruteovergange eller ved hover.
React.preload(productResource);
Dette sikrer, at dataene sandsynligvis er tilgængelige, når komponenten ProductDetails har brug for dem, hvilket minimerer eller eliminerer indlæsningstilstanden.
2. Brug af `Promise.all` til Samtidig Datahentning
En anden simpel og effektiv tilgang er at bruge Promise.all til at starte alle datahentninger samtidigt inden for en enkelt Suspense-grænse. Dette fungerer godt, når dataafhængighederne er kendt på forhånd.
Lad os genbesøge brugerprofileksemplet. I stedet for at hente data sekventielt kan vi hente navn, avatar og aktivitetsstrøm samtidigt:
import { useState, useEffect, Suspense } from 'react';
async function fetchName() {
// Simuler API-kald
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// Simuler API-kald
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// Simuler API-kald
await new Promise(resolve => setTimeout(resolve, 800));
return [
{ id: 1, text: 'Posted a photo' },
{ id: 2, text: 'Updated profile' },
];
}
function useSuspense(promise) {
const [result, setResult] = useState(null);
useEffect(() => {
let didCancel = false;
promise.then(
(data) => {
if (!didCancel) {
setResult({ status: 'success', value: data });
}
},
(error) => {
if (!didCancel) {
setResult({ status: 'error', value: error });
}
}
);
return () => {
didCancel = true;
};
}, [promise]);
if (result?.status === 'success') {
return result.value;
} else if (result?.status === 'error') {
throw result.value;
} else {
throw promise;
}
}
function Name() {
const name = useSuspense(fetchName());
return <h2>{name}</h2>;
}
function Avatar({ name }) {
const avatar = useSuspense(fetchAvatar(name));
return <img src={avatar} alt="User Avatar" />;
}
function Activity({ name }) {
const activity = useSuspense(fetchActivity(name));
return (
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
);
}
function UserProfile() {
const name = useSuspense(fetchName());
return (
<div>
<Suspense fallback=<div>Loading Avatar...</div>>
<Avatar name={name} />
</Suspense>
<Suspense fallback=<div>Loading Activity...</div>>
<Activity name={name} />
</Suspense>
</div>
);
}
export default UserProfile;
Men hvis hver af `Avatar` og `Activity` også er afhængige af `fetchName`, men renderes inde i separate suspense-grænser, kan du løfte `fetchName`-løftet til forælderen og levere det via React Context.
import React, { createContext, useContext, useState, useEffect, Suspense } from 'react';
async function fetchName() {
// Simuler API-kald
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// Simuler API-kald
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// Simuler API-kald
await new Promise(resolve => setTimeout(resolve, 800));
return [
{ id: 1, text: 'Posted a photo' },
{ id: 2, text: 'Updated profile' },
];
}
function useSuspense(promise) {
const [result, setResult] = useState(null);
useEffect(() => {
let didCancel = false;
promise.then(
(data) => {
if (!didCancel) {
setResult({ status: 'success', value: data });
}
},
(error) => {
if (!didCancel) {
setResult({ status: 'error', value: error });
}
}
);
return () => {
didCancel = true;
};
}, [promise]);
if (result?.status === 'success') {
return result.value;
} else if (result?.status === 'error') {
throw result.value;
} else {
throw promise;
}
}
const NamePromiseContext = createContext(null);
function Avatar() {
const namePromise = useContext(NamePromiseContext);
const name = useSuspense(namePromise);
const avatar = useSuspense(fetchAvatar(name));
return <img src={avatar} alt="User Avatar" />;
}
function Activity() {
const namePromise = useContext(NamePromiseContext);
const name = useSuspense(namePromise);
const activity = useSuspense(fetchActivity(name));
return (
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
);
}
function UserProfile() {
const namePromise = fetchName();
return (
<NamePromiseContext.Provider value={namePromise}>
<Suspense fallback=<div>Loading Avatar...</div>>
<Avatar />
</Suspense>
<Suspense fallback=<div>Loading Activity...</div>>
<Activity />
</Suspense>
</NamePromiseContext.Provider>
);
}
export default UserProfile;
3. Brug af et Brugerdefineret Hook til at Administrere Parallelle Hentninger
For mere komplekse scenarier med potentielt betingede dataafhængigheder kan du oprette et brugerdefineret hook til at administrere den parallelle datahentning og returnere en ressource, som Suspense kan bruge.
import { useState, useEffect, useRef } from 'react';
function useParallelData(fetchFunctions) {
const [resource, setResource] = useState(null);
const mounted = useRef(true);
useEffect(() => {
mounted.current = true;
const promises = fetchFunctions.map(fn => fn());
const suspender = Promise.all(promises).then(
(results) => {
if (mounted.current) {
setResource({ status: 'success', value: results });
}
},
(error) => {
if (mounted.current) {
setResource({ status: 'error', value: error });
}
}
);
setResource({
status: 'pending',
value: suspender,
});
return () => {
mounted.current = false;
};
}, [fetchFunctions]);
const read = () => {
if (!resource) {
throw new Error('Resource not yet initialized');
}
if (resource.status === 'pending') {
throw resource.value;
}
if (resource.status === 'error') {
throw resource.value;
}
return resource.value;
};
return { read };
}
// Eksempelbrug:
async function fetchUserData(userId) {
// Simuler API-kald
await new Promise(resolve => setTimeout(resolve, 300));
return { id: userId, name: 'User ' + userId };
}
async function fetchUserPosts(userId) {
// Simuler API-kald
await new Promise(resolve => setTimeout(resolve, 500));
return [{ id: 1, title: 'Post 1' }, { id: 2, title: 'Post 2' }];
}
function UserProfile({ userId }) {
const { read } = useParallelData([
() => fetchUserData(userId),
() => fetchUserPosts(userId),
]);
const [userData, userPosts] = read();
return (
<div>
<h2>{userData.name}</h2>
<ul>
{userPosts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
</div>
);
}
function App() {
return (
<Suspense fallback=<div>Loading user data...</div>>
<UserProfile userId={123} />
</Suspense>
);
}
export default App;
Denne tilgang indkapsler kompleksiteten ved at administrere løfterne og indlæsningstilstandene i hooket, hvilket gør komponentkoden renere og mere fokuseret på rendering af dataene.
4. Selektiv Hydrering med Streaming Server Rendering
For server-renderede applikationer introducerer React 18 selektiv hydrering med streaming server rendering. Dette giver dig mulighed for at sende HTML til klienten i bidder, efterhånden som det bliver tilgængeligt på serveren. Du kan ombryde langsomt indlæsende komponenter med <Suspense>-grænser, så resten af siden kan blive interaktiv, mens de langsomme komponenter stadig indlæses på serveren. Dette forbedrer den opfattede ydeevne dramatisk, især for brugere med langsomme netværksforbindelser eller enheder.
Overvej et scenarie, hvor et nyhedswebsted skal vise artikler fra forskellige regioner i verden (f.eks. Asien, Europa, Amerika). Nogle datakilder kan være langsommere end andre. Selektiv hydrering giver mulighed for at vise artikler fra hurtigere regioner først, mens dem fra langsommere regioner stadig indlæses, hvilket forhindrer hele siden i at blive blokeret.
Håndtering af Fejl og Indlæsningstilstande
Mens Suspense forenkler administration af indlæsningstilstand, er fejlhåndtering stadig afgørende. Fejlgrænser (ved hjælp af componentDidCatch-livscyklusmetoden eller useErrorBoundary-hooket fra biblioteker som `react-error-boundary`) giver dig mulighed for elegant at håndtere fejl, der opstår under datahentning eller rendering. Disse fejlgrænser bør placeres strategisk for at fange fejl inden for specifikke Suspense-grænser, hvilket forhindrer hele applikationen i at gå ned.
import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
function MyComponent() {
// ... henter data, der kan give fejl
}
function App() {
return (
<ErrorBoundary fallback={<div>Noget gik galt!</div>}>
<Suspense fallback={<div>Indlæser...</div>}>
<MyComponent />
</Suspense>
</ErrorBoundary>
);
}
Husk at levere informativ og brugervenlig fallback-UI til både indlæsnings- og fejltilstande. Dette er især vigtigt for internationale brugere, der kan opleve langsommere netværkshastigheder eller regionale serviceudfald.
Bedste Praksis for Optimering af Datahentning med Suspense
- Identificer og Prioriter Kritiske Data: Bestem, hvilke data der er essentielle for den indledende rendering af din applikation, og prioriter hentning af disse data først.
- Forudindlæs Data Når Det Er Muligt: Brug `React.preload` og ressourcer til at forudindlæse data, før komponenter har brug for det, hvilket minimerer indlæsningstilstande.
- Hent Data Samtidigt: Brug `Promise.all` eller brugerdefinerede hooks til at starte flere datahentninger parallelt.
- Optimer API-endepunkter: Sørg for, at dine API-endepunkter er optimeret til ydeevne, hvilket minimerer latens og nyttelaststørrelse. Overvej at bruge teknikker som GraphQL til kun at hente de data, du har brug for.
- Implementer Caching: Cache ofte tilgåede data for at reducere antallet af API-anmodninger. Overvej at bruge biblioteker som `swr` eller `react-query` til robuste caching-funktioner.
- Brug Kodeopdeling: Opdel din applikation i mindre bidder for at reducere den indledende indlæsningstid. Kombiner kodeopdeling med Suspense for gradvist at indlæse og rendere forskellige dele af din applikation.
- Overvåg Ydeevne: Overvåg regelmæssigt din applikations ydeevne ved hjælp af værktøjer som Lighthouse eller WebPageTest for at identificere og adressere ydelsesflaskehalse.
- Håndter Fejl Elegant: Implementer fejlgrænser for at fange fejl under datahentning og rendering, og giv informative fejlmeddelelser til brugerne.
- Overvej Server-Side Rendering (SSR): Af SEO- og ydelsesmæssige årsager bør du overveje at bruge SSR med streaming og selektiv hydrering for at levere en hurtigere indledende oplevelse.
Konklusion
React Suspense, når det kombineres med strategier til parallel datahentning, giver et kraftfuldt værktøjssæt til at bygge responsive og velfungerende webapplikationer. Ved at forstå vandfaldsproblemet og implementere teknikker som forudindlæsning, samtidig hentning med Promise.all og brugerdefinerede hooks kan du forbedre brugeroplevelsen markant. Husk at håndtere fejl elegant og overvåge ydeevnen for at sikre, at din applikation forbliver optimeret til brugere over hele verden. Efterhånden som React fortsætter med at udvikle sig, vil udforskning af nye funktioner som selektiv hydrering med streaming server rendering yderligere forbedre din evne til at levere exceptionelle brugeroplevelser, uanset placering eller netværksforhold. Ved at omfavne disse teknikker kan du skabe applikationer, der ikke kun er funktionelle, men også en fornøjelse at bruge for dit globale publikum.
Dette blogindlæg har til formål at give et omfattende overblik over parallelle datahentningsstrategier med React Suspense. Vi håber, du fandt det informativt og hjælpsomt. Vi opfordrer dig til at eksperimentere med disse teknikker i dine egne projekter og dele dine resultater med fællesskabet.