Utforska avancerade tekniker för parallell datainhÀmtning i React med Suspense, vilket förbÀttrar applikationens prestanda och anvÀndarupplevelse.
React Suspense-koordinering: Att bemÀstra parallell datainhÀmtning
React Suspense har revolutionerat hur vi hanterar asynkrona operationer, sÀrskilt datainhÀmtning. Det tillÄter komponenter att "suspendera" rendering medan de vÀntar pÄ att data ska laddas, vilket ger ett deklarativt sÀtt att hantera laddningstillstÄnd. Men att helt enkelt wrappa enskilda datahÀmtningar med Suspense kan leda till en vattenfallseffekt, dÀr en hÀmtning slutförs innan nÀsta startar, vilket negativt pÄverkar prestandan. Detta blogginlÀgg fördjupar sig i avancerade strategier för att samordna flera datahÀmtningar parallellt med Suspense, optimera din applikations responsivitet och förbÀttra anvÀndarupplevelsen för en global publik.
Att förstÄ vattenfallsproblemet vid datainhÀmtning
FörestÀll dig ett scenario dÀr du behöver visa en anvÀndarprofil med deras namn, avatar och senaste aktivitet. Om du hÀmtar varje datastycke sekventiellt ser anvÀndaren en laddningssnurra för namnet, sedan en annan för avataren och slutligen en för aktivitetsflödet. Detta sekventiella laddningsmönster skapar en vattenfallseffekt, vilket fördröjer renderingen av den kompletta profilen och frustrerar anvÀndarna. För internationella anvÀndare med varierande nÀtverkshastigheter kan denna fördröjning bli Ànnu mer uttalad.
TÀnk pÄ detta förenklade kodavsnitt:
function UserProfile() {
const name = useName(); // HÀmtar anvÀndarnamn
const avatar = useAvatar(name); // HÀmtar avatar baserat pÄ namn
const activity = useActivity(name); // HÀmtar aktivitet baserat pÄ namn
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 detta exempel Ă€r useAvatar och useActivity beroende av resultatet av useName. Detta skapar ett tydligt vattenfall â useAvatar och useActivity kan inte börja hĂ€mta data förrĂ€n useName slutförts. Detta Ă€r ineffektivt och en vanlig prestandaflaskhals.
Strategier för parallell datainhÀmtning med Suspense
Nyckeln till att optimera datainhÀmtning med Suspense Àr att initiera alla dataförfrÄgningar samtidigt. HÀr Àr flera strategier du kan anvÀnda:
1. Förladda data med `React.preload` och resurser
En av de mest kraftfulla teknikerna Àr att förladda data innan komponenten ens renderas. Detta innebÀr att skapa en "resurs" (ett objekt som kapslar in datahÀmtningslöftet) och förhÀmta data. `React.preload` hjÀlper till med detta. NÀr komponenten behöver data Àr den redan tillgÀnglig, vilket eliminerar laddningstillstÄndet nÀstan helt.
TÀnk pÄ en resurs för att hÀmta en 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;
},
};
};
// AnvÀndning:
const productResource = createProductResource(123);
function ProductDetails() {
const product = productResource.read();
return (<div>{product.name}</div>);
}
Nu kan du förladda denna resurs innan ProductDetails-komponenten renderas. Till exempel under ruttövergÄngar eller vid hovring.
React.preload(productResource);
Detta sÀkerstÀller att data sannolikt Àr tillgÀngliga nÀr ProductDetails-komponenten behöver dem, vilket minimerar eller eliminerar laddningstillstÄndet.
2. AnvÀnda `Promise.all` för samtidig datainhÀmtning
En annan enkel och effektiv metod Àr att anvÀnda Promise.all för att initiera alla datahÀmtningar samtidigt inom en enda Suspense-grÀns. Detta fungerar bra nÀr databeroenden Àr kÀnda i förvÀg.
LÄt oss Äterbesöka anvÀndarprofilexemplet. IstÀllet för att hÀmta data sekventiellt kan vi hÀmta namn, avatar och aktivitetsflöde samtidigt:
import { useState, useEffect, Suspense } from 'react';
async function fetchName() {
// Simulera API-anrop
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// Simulera API-anrop
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// Simulera API-anrop
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 om Avatar och Activity ocksÄ förlitar sig pÄ fetchName, men renderas inom separata suspensgrÀnser, kan du lyfta fetchName-löftet till förÀldern och tillhandahÄlla det via React Context.
import React, { createContext, useContext, useState, useEffect, Suspense } from 'react';
async function fetchName() {
// Simulera API-anrop
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// Simulera API-anrop
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// Simulera API-anrop
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. AnvÀnda en anpassad Hook för att hantera parallella hÀmtningar
För mer komplexa scenarier med potentiellt villkorliga databeroenden kan du skapa en anpassad hook för att hantera den parallella datainhÀmtningen och returnera en resurs som Suspense kan anvÀnda.
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 };
}
// Exempel anvÀndning:
async function fetchUserData(userId) {
// Simulera API-anrop
await new Promise(resolve => setTimeout(resolve, 300));
return { id: userId, name: 'User ' + userId };
}
async function fetchUserPosts(userId) {
// Simulera API-anrop
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;
Denna metod inkapslar komplexiteten i att hantera löften och laddningstillstÄnd inom hooken, vilket gör komponentkoden renare och mer fokuserad pÄ att rendera data.
4. Selektiv hydrering med streaming server rendering
För serverrenderade applikationer introducerar React 18 selektiv hydrering med streaming server rendering. Detta gör att du kan skicka HTML till klienten i bitar allt eftersom den blir tillgÀnglig pÄ servern. Du kan wrappa lÄngsamt laddande komponenter med <Suspense>-grÀnser, vilket gör att resten av sidan blir interaktiv medan de lÄngsamma komponenterna fortfarande laddas pÄ servern. Detta förbÀttrar avsevÀrt den upplevda prestandan, sÀrskilt för anvÀndare med lÄngsamma nÀtverksanslutningar eller enheter.
TÀnk dig ett scenario dÀr en nyhetswebbplats behöver visa artiklar frÄn olika regioner i vÀrlden (t.ex. Asien, Europa, Amerika). Vissa datakÀllor kan vara lÄngsammare Àn andra. Selektiv hydrering gör det möjligt att visa artiklar frÄn snabbare regioner först, medan de frÄn lÄngsammare regioner fortfarande laddas, vilket förhindrar att hela sidan blockeras.
Hantering av fel och laddningstillstÄnd
Medan Suspense förenklar hanteringen av laddningstillstÄnd Àr felhantering fortfarande avgörande. FelgrÀnser (med hjÀlp av componentDidCatch livscykelmetoden eller useErrorBoundary-hooken frÄn bibliotek som `react-error-boundary`) gör att du elegant kan hantera fel som uppstÄr under datainhÀmtning eller rendering. Dessa felgrÀnser bör placeras strategiskt för att fÄnga fel inom specifika Suspense-grÀnser, vilket förhindrar att hela applikationen kraschar.
import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
function MyComponent() {
// ... hÀmtar data som kan generera fel
}
function App() {
return (
<ErrorBoundary fallback={<div>Something went wrong!</div>}>
<Suspense fallback={<div>Loading...</div>}>
<MyComponent />
</Suspense>
</ErrorBoundary>
);
}
Kom ihÄg att tillhandahÄlla informativt och anvÀndarvÀnligt fallback-UI för bÄde laddnings- och feltillstÄnd. Detta Àr sÀrskilt viktigt för internationella anvÀndare som kan stöta pÄ lÄngsammare nÀtverkshastigheter eller regionala serviceavbrott.
BÀsta praxis för att optimera datainhÀmtning med Suspense
- Identifiera och prioritera kritisk data: Avgör vilken data som Àr vÀsentlig för den initiala renderingen av din applikation och prioritera att hÀmta den datan först.
- Förladda data nÀr det Àr möjligt: AnvÀnd `React.preload` och resurser för att förladda data innan komponenter behöver det, vilket minimerar laddningstillstÄnd.
- HÀmta data samtidigt: AnvÀnd `Promise.all` eller anpassade hooks för att initiera flera datahÀmtningar parallellt.
- Optimera API-slutpunkter: Se till att dina API-slutpunkter Ă€r optimerade för prestanda, vilket minimerar latens och payloadstorlek. ĂvervĂ€g att anvĂ€nda tekniker som GraphQL för att hĂ€mta bara den data du behöver.
- Implementera caching: Cache:a data som anvĂ€nds ofta för att minska antalet API-förfrĂ„gningar. ĂvervĂ€g att anvĂ€nda bibliotek som `swr` eller `react-query` för robusta cachingmöjligheter.
- AnvÀnd kodsplittring: Dela upp din applikation i mindre bitar för att minska den initiala laddningstiden. Kombinera kodsplittring med Suspense för att gradvis ladda och rendera olika delar av din applikation.
- Ăvervaka prestanda: Ăvervaka regelbundet din applikations prestanda med verktyg som Lighthouse eller WebPageTest för att identifiera och Ă„tgĂ€rda prestandaflaskhalsar.
- Hantering av fel pÄ ett elegant sÀtt: Implementera felgrÀnser för att fÄnga fel under datainhÀmtning och rendering, vilket ger informativa felmeddelanden till anvÀndarna.
- ĂvervĂ€g server-side rendering (SSR): Av SEO- och prestandaskĂ€l, övervĂ€g att anvĂ€nda SSR med streaming och selektiv hydrering för att leverera en snabbare initial upplevelse.
Slutsats
React Suspense, i kombination med strategier för parallell datainhÀmtning, tillhandahÄller en kraftfull verktygslÄda för att bygga responsiva och högpresterande webbapplikationer. Genom att förstÄ vattenfallsproblemet och implementera tekniker som förladdning, samtidig hÀmtning med Promise.all och anpassade hooks, kan du avsevÀrt förbÀttra anvÀndarupplevelsen. Kom ihÄg att hantera fel pÄ ett elegant sÀtt och övervaka prestanda för att sÀkerstÀlla att din applikation förblir optimerad för anvÀndare över hela vÀrlden. NÀr React fortsÀtter att utvecklas kommer utforskningen av nya funktioner som selektiv hydrering med streaming server rendering ytterligare att förbÀttra din förmÄga att leverera exceptionella anvÀndarupplevelser, oavsett plats eller nÀtverksförhÄllanden. Genom att omfamna dessa tekniker kan du skapa applikationer som inte bara Àr funktionella utan ocksÄ en glÀdje att anvÀnda för din globala publik.
Detta blogginlÀgg har syftat till att ge en omfattande översikt över parallella datainhÀmtningsstrategier med React Suspense. Vi hoppas att du tyckte att det var informativt och hjÀlpsamt. Vi uppmuntrar dig att experimentera med dessa tekniker i dina egna projekt och dela dina resultat med communityn.