Istražite napredne tehnike paralelnog dohvaćanja podataka u Reactu pomoću Suspense, poboljšavajući performanse i korisničko iskustvo.
Koordinacija React Suspense: Ovladavanje paralelnim dohvaćanjem podataka
React Suspense revolucionirao je način na koji obrađujemo asinkrone operacije, posebno dohvaćanje podataka. Omogućuje komponentama da "suspendiraju" renderiranje dok čekaju učitavanje podataka, pružajući deklarativni način upravljanja stanjima učitavanja. Međutim, jednostavno omotavanje pojedinačnih dohvaćanja podataka pomoću Suspense može dovesti do efekta vodopada, gdje jedno dohvaćanje završava prije nego što drugo započne, negativno utječući na performanse. Ovaj blog post bavi se naprednim strategijama za koordinaciju više dohvaćanja podataka paralelno pomoću Suspense, optimizirajući odzivnost vaše aplikacije i poboljšavajući korisničko iskustvo za globalnu publiku.
Razumijevanje problema vodopada u dohvaćanju podataka
Zamislite scenarij u kojem trebate prikazati korisnički profil s njihovim imenom, avatarom i nedavnim aktivnostima. Ako dohvaćate svaku stavku podataka sekvencijalno, korisnik vidi spinner za učitavanje za ime, zatim još jedan za avatar, i na kraju, jedan za feed aktivnosti. Ovaj sekvencijalni obrazac učitavanja stvara efekt vodopada, odgađajući renderiranje potpunog profila i frustrirajući korisnike. Za međunarodne korisnike s različitim brzinama mreže, ovo kašnjenje može biti još izraženije.
Razmotrite ovaj pojednostavljeni isječak koda:
function UserProfile() {
const name = useName(); // Dohvaća korisničko ime
const avatar = useAvatar(name); // Dohvaća avatar na temelju imena
const activity = useActivity(name); // Dohvaća aktivnost na temelju imena
return (
<div>
<h2>{name}</h2>
<img src={avatar} alt="Korisnički avatar" />
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
</div>
);
}
U ovom primjeru, useAvatar i useActivity ovise o rezultatu useName. Ovo stvara jasan vodopad – useAvatar i useActivity ne mogu započeti dohvaćanje podataka dok se useName ne završi. Ovo je neučinkovito i uobičajeno usko grlo u performansama.
Strategije za paralelno dohvaćanje podataka sa Suspense
Ključ optimizacije dohvaćanja podataka sa Suspense je pokretanje svih zahtjeva za dohvaćanje podataka istovremeno. Evo nekoliko strategija koje možete primijeniti:
1. Prethodno učitavanje podataka s `React.preload` i resursima
Jedna od najmoćnijih tehnika je prethodno učitavanje podataka prije nego što komponenta uopće renderira. To uključuje stvaranje "resursa" (objekta koji inkapsulira obećanje dohvaćanja podataka) i prethodno dohvaćanje podataka. `React.preload` pomaže u tome. Do trenutka kada komponenti trebaju podaci, oni su već dostupni, eliminirajući gotovo u potpunosti stanje učitavanja.
Razmotrite resurs za dohvaćanje proizvoda:
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 greška! 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;
},
};
};
// Upotreba:
const productResource = createProductResource(123);
function ProductDetails() { //Detalji proizvoda
const product = productResource.read();
return (<div>{product.name}</div>);
}
Sada možete prethodno učitati ovaj resurs prije nego što se komponenta ProductDetails renderira. Na primjer, tijekom prijelaza rute ili na prelet mišem.
React.preload(productResource);
Ovo osigurava da su podaci vjerojatno dostupni do trenutka kada ih komponenta ProductDetails zatreba, minimizirajući ili eliminirajući stanje učitavanja.
2. Korištenje `Promise.all` za istovremeno dohvaćanje podataka
Drugi jednostavan i učinkovit pristup je korištenje Promise.all za istovremeno pokretanje svih dohvaćanja podataka unutar jedne Suspense granice. Ovo dobro funkcionira kada su ovisnosti podataka poznate unaprijed.
Vratimo se na primjer korisničkog profila. Umjesto sekvencijalnog dohvaćanja podataka, možemo istovremeno dohvaćati ime, avatar i feed aktivnosti:
import { useState, useEffect, Suspense } from 'react';
async function fetchName() {
// Simulacija API poziva
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// Simulacija API poziva
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// Simulacija API poziva
await new Promise(resolve => setTimeout(resolve, 800));
return [
{ id: 1, text: 'Objavio fotografiju' },
{ id: 2, text: 'Ažurirao profil' },
];
}
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="Korisnički 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>Učitavanje avatara...</div>}>
<Avatar name={name} />
</Suspense>
<Suspense fallback={<div>Učitavanje aktivnosti...</div>}>
<Activity name={name} />
</Suspense>
</div>
);
}
export default UserProfile;
Međutim, ako svaki od Avatar i Activity također ovise o fetchName, ali su renderirani unutar odvojenih Suspense granica, možete podići fetchName obećanje na roditeljsku komponentu i pružiti ga putem React Contexta.
import React, { createContext, useContext, useState, useEffect, Suspense } from 'react';
async function fetchName() {
// Simulacija API poziva
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// Simulacija API poziva
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// Simulacija API poziva
await new Promise(resolve => setTimeout(resolve, 800));
return [
{ id: 1, text: 'Objavio fotografiju' },
{ id: 2, text: 'Ažurirao profil' },
];
}
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="Korisnički 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>Učitavanje avatara...</div>}>
<Avatar />
</Suspense>
<Suspense fallback={<div>Učitavanje aktivnosti...</div>}>
<Activity />
</Suspense>
</NamePromiseContext.Provider>
);
}
export default UserProfile;
3. Korištenje prilagođenog hooka za upravljanje paralelnim dohvaćanjima
Za složenije scenarije s potencijalno uvjetnim ovisnostima podataka, možete stvoriti prilagođeni hook za upravljanje paralelnim dohvaćanjem podataka i vratiti resurs koji Suspense može koristiti.
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('Resurs još nije inicijaliziran');
}
if (resource.status === 'pending') {
throw resource.value;
}
if (resource.status === 'error') {
throw resource.value;
}
return resource.value;
};
return { read };
}
// Primjer upotrebe:
async function fetchUserData(userId) {
// Simulacija API poziva
await new Promise(resolve => setTimeout(resolve, 300));
return { id: userId, name: 'Korisnik ' + userId };
}
async function fetchUserPosts(userId) {
// Simulacija API poziva
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>Učitavanje korisničkih podataka...</div>}>
<UserProfile userId={123} />
</Suspense>
);
}
export default App;
Ovaj pristup inkapsulira složenost upravljanja obećanjima i stanjima učitavanja unutar hooka, čineći kod komponente čišćim i fokusiranijim na renderiranje podataka.
4. Selektivna hidratacija sa streamiranim renderiranjem na strani poslužitelja
Za aplikacije renderirane na strani poslužitelja, React 18 uvodi selektivnu hidrataciju sa streamiranim renderiranjem na strani poslužitelja. Ovo vam omogućuje slanje HTML-a klijentu u dijelovima kako postaje dostupan na poslužitelju. Možete omotati komponente koje se sporo učitavaju s <Suspense> granicama, omogućujući ostatku stranice da postane interaktivna dok se spore komponente još uvijek učitavaju na poslužitelju. Ovo dramatično poboljšava percipirane performanse, posebno za korisnike s sporim internetskim vezama ili uređajima.
Razmotrite scenarij gdje web stranica s vijestima treba prikazati članke iz različitih regija svijeta (npr. Azija, Europa, Amerike). Neki izvori podataka mogu biti sporiji od drugih. Selektivna hidratacija omogućuje prvo prikazivanje članaka iz bržih regija, dok se oni iz sporijih još uvijek učitavaju, sprječavajući blokiranje cijele stranice.
Upravljanje greškama i stanjima učitavanja
Dok Suspense pojednostavljuje upravljanje stanjem učitavanja, upravljanje greškama ostaje ključno. Granice grešaka (korištenjem životnog ciklusa componentDidCatch ili hooka useErrorBoundary iz biblioteka poput `react-error-boundary`) omogućuju vam da na elegantan način obrađujete greške koje se pojave tijekom dohvaćanja podataka ili renderiranja. Ove granice grešaka trebaju biti strateški postavljene da hvataju greške unutar određenih Suspense granica, sprječavajući pad cijele aplikacije.
import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
function MyComponent() {
// ... dohvaća podatke koji mogu izazvati grešku
}
function App() {
return (
<ErrorBoundary fallback={<div>Nešto je pošlo po zlu!</div>}>
<Suspense fallback={<div>Učitavanje...</div>}>
<MyComponent />
</Suspense>
</ErrorBoundary>
);
}
Ne zaboravite pružiti informativno i korisnički prijateljsko fallback korisničko sučelje kako za stanja učitavanja, tako i za greške. Ovo je posebno važno za međunarodne korisnike koji možda doživljavaju sporije brzine mreže ili regionalne prekide usluga.
Najbolje prakse za optimizaciju dohvaćanja podataka sa Suspense
- Identificirajte i prioritizirajte kritične podatke: Odredite koji su podaci neophodni za početno renderiranje vaše aplikacije i dajte prioritet dohvaćanju tih podataka prvi.
- Prethodno učitavajte podatke kad god je moguće: Koristite `React.preload` i resurse za prethodno učitavanje podataka prije nego što su komponentama potrebni, minimizirajući stanja učitavanja.
- Dohvaćajte podatke istovremeno: Iskoristite `Promise.all` ili prilagođene hookove za pokretanje više dohvaćanja podataka paralelno.
- Optimizirajte API krajnje točke: Osigurajte da su vaše API krajnje točke optimizirane za performanse, minimizirajući latenciju i veličinu payload-a. Razmislite o korištenju tehnika poput GraphQL-a za dohvaćanje samo podataka koji vam trebaju.
- Implementirajte predmemoriju (Caching): Predmemorirajte često korištene podatke kako biste smanjili broj API zahtjeva. Razmislite o korištenju biblioteka poput `swr` ili `react-query` za robusne mogućnosti predmemoriranja.
- Koristite dijeljenje koda (Code Splitting): Podijelite svoju aplikaciju na manje dijelove kako biste smanjili početno vrijeme učitavanja. Kombinirajte dijeljenje koda sa Suspense za progresivno učitavanje i renderiranje različitih dijelova vaše aplikacije.
- Pratite performanse: Redovito pratite performanse svoje aplikacije pomoću alata poput Lighthouse ili WebPageTest kako biste identificirali i riješili usko grlo u performansama.
- Upravljajte greškama na elegantan način: Implementirajte granice grešaka za hvatanje grešaka tijekom dohvaćanja podataka i renderiranja, pružajući informativne poruke o greškama korisnicima.
- Razmotrite renderiranje na strani poslužitelja (SSR): Iz razloga SEO-a i performansi, razmotrite korištenje SSR-a sa streamiranjem i selektivnom hidratacijom kako biste isporučili brže početno iskustvo.
Zaključak
React Suspense, u kombinaciji sa strategijama za paralelno dohvaćanje podataka, pruža moćan alat za izgradnju responzivnih i performantnih web aplikacija. Razumijevanjem problema vodopada i implementacijom tehnika poput prethodnog učitavanja, istovremenog dohvaćanja s Promise.all i prilagođenih hookova, možete značajno poboljšati korisničko iskustvo. Ne zaboravite na elegantno upravljanje greškama i praćenje performansi kako biste osigurali da vaša aplikacija ostane optimizirana za korisnike širom svijeta. Kako se React nastavlja razvijati, istraživanje novih značajki poput selektivne hidratacije sa streamiranim renderiranjem na strani poslužitelja dodatno će poboljšati vašu sposobnost isporuke iznimnih korisničkih iskustava, bez obzira na lokaciju ili mrežne uvjete. Prihvaćanjem ovih tehnika možete stvoriti aplikacije koje nisu samo funkcionalne, već i zadovoljstvo za korištenje vašoj globalnoj publici.
Ovaj blog post imao je za cilj pružiti sveobuhvatan pregled strategija paralelnog dohvaćanja podataka sa React Suspense. Nadamo se da ste ga smatrali informativnim i korisnim. Potičemo vas da eksperimentirate s ovim tehnikama u vlastitim projektima i podijelite svoja otkrića sa zajednicom.