Explorez des techniques avancées pour la récupération parallèle de données dans React avec Suspense, améliorant la performance et l'expérience utilisateur.
Coordination React Suspense : Maîtriser la récupération de données parallèle
React Suspense a révolutionné notre façon de gérer les opérations asynchrones, en particulier la récupération de données. Il permet aux composants de "suspendre" le rendu en attendant que les données soient chargées, offrant une manière déclarative de gérer les états de chargement. Cependant, le simple fait d'envelopper des récupérations de données individuelles avec Suspense peut entraîner un effet de cascade, où une récupération se termine avant le début de la suivante, nuisant aux performances. Ce billet de blog explore des stratégies avancées pour coordonner plusieurs récupérations de données en parallèle en utilisant Suspense, optimisant la réactivité de votre application et améliorant l'expérience utilisateur pour une audience mondiale.
Comprendre le problème de la cascade dans la récupération de données
Imaginez un scénario où vous devez afficher le profil d'un utilisateur avec son nom, son avatar et son activité récente. Si vous récupérez chaque élément de données séquentiellement, l'utilisateur voit un spinner de chargement pour le nom, puis un autre pour l'avatar, et enfin, un pour le flux d'activité. Ce schéma de chargement séquentiel crée un effet de cascade, retardant le rendu du profil complet et frustrant les utilisateurs. Pour les utilisateurs internationaux avec des vitesses de réseau variables, ce délai peut être encore plus prononcé.
Considérez cet extrait de code simplifié :
function UserProfile() {
const name = useName(); // Récupère le nom de l'utilisateur
const avatar = useAvatar(name); // Récupère l'avatar basé sur le nom
const activity = useActivity(name); // Récupère l'activité basée sur le nom
return (
<div>
<h2>{name}</h2>
<img src={avatar} alt="User Avatar" />
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
</div>
);
}
Dans cet exemple, useAvatar et useActivity dépendent du résultat de useName. Cela crée une cascade claire : useAvatar et useActivity ne peuvent pas commencer à récupérer les données tant que useName n'est pas terminé. Ceci est inefficace et constitue un goulot d'étranglement de performance courant.
Stratégies pour la récupération de données parallèle avec Suspense
La clé pour optimiser la récupération de données avec Suspense est d'initier toutes les requêtes de données simultanément. Voici plusieurs stratégies que vous pouvez employer :
1. Précharger les données avec `React.preload` et les ressources
L'une des techniques les plus puissantes est de précharger les données avant même que le composant ne soit rendu. Cela implique de créer une "ressource" (un objet qui encapsule la promesse de récupération de données) et de pré-récupérer les données. `React.preload` aide à cela. Au moment où le composant a besoin des données, elles sont déjà disponibles, éliminant presque entièrement l'état de chargement.
Considérez une ressource pour récupérer un produit :
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;
},
};
};
// Utilisation :
const productResource = createProductResource(123);
function ProductDetails() {
const product = productResource.read();
return (<div>{product.name}</div>);
}
Maintenant, vous pouvez précharger cette ressource avant que le composant ProductDetails ne soit rendu. Par exemple, lors des transitions de route ou au survol.
React.preload(productResource);
Cela garantit que les données sont probablement disponibles au moment où le composant ProductDetails en a besoin, minimisant ou éliminant l'état de chargement.
2. Utilisation de `Promise.all` pour la récupération de données concurrente
Une autre approche simple et efficace consiste à utiliser Promise.all pour initier toutes les récupérations de données simultanément au sein d'une seule frontière Suspense. Cela fonctionne bien lorsque les dépendances de données sont connues à l'avance.
Revisitons l'exemple du profil utilisateur. Au lieu de récupérer les données séquentiellement, nous pouvons récupérer le nom, l'avatar et le fil d'activité simultanément :
import { useState, useEffect, Suspense } from 'react';
async function fetchName() {
// Simule un appel API
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// Simule un appel API
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// Simule un appel API
await new Promise(resolve => setTimeout(resolve, 800));
return [
{ id: 1, text: 'A posté une photo' },
{ id: 2, text: 'A mis Ă jour son 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="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>Chargement de l'avatar...</div>}>
<Avatar name={name} />
</Suspense>
<Suspense fallback={<div>Chargement de l'activité...</div>}>
<Activity name={name} />
</Suspense>
</div>
);
}
export default UserProfile;
Cependant, si chacun de Avatar et Activity dépend également de fetchName, mais rendu dans des frontières Suspense séparées, vous pouvez remonter la promesse de fetchName au parent et la fournir via React Context.
import React, { createContext, useContext, useState, useEffect, Suspense } from 'react';
async function fetchName() {
// Simule un appel API
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// Simule un appel API
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// Simule un appel API
await new Promise(resolve => setTimeout(resolve, 800));
return [
{ id: 1, text: 'A posté une photo' },
{ id: 2, text: 'A mis Ă jour son 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="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>Chargement de l'avatar...</div>}>
<Avatar />
</Suspense>
<Suspense fallback={<div>Chargement de l'activité...</div>}>
<Activity />
</Suspense>
</NamePromiseContext.Provider>
);
}
export default UserProfile;
3. Utilisation d'un hook personnalisé pour gérer les récupérations parallèles
Pour des scénarios plus complexes avec des dépendances de données potentiellement conditionnelles, vous pouvez créer un hook personnalisé pour gérer la récupération parallèle des données et retourner une ressource que Suspense peut utiliser.
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('Ressource pas encore initialisée');
}
if (resource.status === 'pending') {
throw resource.value;
}
if (resource.status === 'error') {
throw resource.value;
}
return resource.value;
};
return { read };
}
// Exemple d'utilisation :
async function fetchUserData(userId) {
// Simule un appel API
await new Promise(resolve => setTimeout(resolve, 300));
return { id: userId, name: 'Utilisateur ' + userId };
}
async function fetchUserPosts(userId) {
// Simule un appel API
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>Chargement des données utilisateur...</div>}>
<UserProfile userId={123} />
</Suspense>
);
}
export default App;
Cette approche encapsule la complexité de la gestion des promesses et des états de chargement dans le hook, rendant le code du composant plus propre et plus axé sur le rendu des données.
4. Hydratation sélective avec le rendu serveur en streaming
Pour les applications rendues côté serveur, React 18 introduit l'hydratation sélective avec le rendu serveur en streaming. Cela vous permet d'envoyer du HTML au client par morceaux dès qu'il est disponible sur le serveur. Vous pouvez envelopper les composants à chargement lent avec des frontières <Suspense>, permettant au reste de la page de devenir interactif pendant que les composants lents sont toujours en cours de chargement sur le serveur. Cela améliore considérablement les performances perçues, en particulier pour les utilisateurs ayant des connexions réseau ou des appareils lents.
Considérez un scénario où un site d'actualités doit afficher des articles de différentes régions du monde (par exemple, Asie, Europe, Amériques). Certaines sources de données peuvent être plus lentes que d'autres. L'hydratation sélective permet d'afficher d'abord les articles des régions les plus rapides, tandis que ceux des régions plus lentes sont encore en cours de chargement, empêchant le blocage de toute la page.
Gestion des erreurs et des états de chargement
Bien que Suspense simplifie la gestion des états de chargement, la gestion des erreurs reste cruciale. Les frontières d'erreurs (en utilisant la méthode de cycle de vie componentDidCatch ou le hook useErrorBoundary de bibliothèques comme `react-error-boundary`) vous permettent de gérer gracieusement les erreurs qui surviennent lors de la récupération de données ou du rendu. Ces frontières d'erreurs doivent être placées stratégiquement pour capturer les erreurs au sein de frontières Suspense spécifiques, empêchant l'application entière de planter.
import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
function MyComponent() {
// ... récupère des données qui peuvent générer une erreur
}
function App() {
return (
<ErrorBoundary fallback={<div>Quelque chose s'est mal passé !</div>}>
<Suspense fallback={<div>Chargement...</div>}>
<MyComponent />
</Suspense>
</ErrorBoundary>
);
}
N'oubliez pas de fournir une interface utilisateur de secours informative et conviviale pour les états de chargement et d'erreur. Ceci est particulièrement important pour les utilisateurs internationaux qui pourraient rencontrer des vitesses de réseau plus lentes ou des pannes de service régionales.
Meilleures pratiques pour optimiser la récupération de données avec Suspense
- Identifier et prioriser les données critiques : Déterminez quelles données sont essentielles au rendu initial de votre application et priorisez la récupération de ces données en premier.
- Précharger les données lorsque c'est possible : Utilisez `React.preload` et les ressources pour précharger les données avant que les composants en aient besoin, minimisant les états de chargement.
- Récupérer les données simultanément : Utilisez `Promise.all` ou des hooks personnalisés pour initier plusieurs récupérations de données en parallèle.
- Optimiser les points de terminaison API : Assurez-vous que vos points de terminaison API sont optimisés pour les performances, en minimisant la latence et la taille de la charge utile. Envisagez d'utiliser des techniques comme GraphQL pour récupérer uniquement les données dont vous avez besoin.
- Implémenter la mise en cache : Mettez en cache les données fréquemment consultées pour réduire le nombre de requêtes API. Envisagez d'utiliser des bibliothèques comme `swr` ou `react-query` pour des capacités de mise en cache robustes.
- Utiliser la séparation de code : Séparez votre application en plus petits morceaux pour réduire le temps de chargement initial. Combinez la séparation de code avec Suspense pour charger et rendre progressivement différentes parties de votre application.
- Surveiller les performances : Surveillez régulièrement les performances de votre application à l'aide d'outils comme Lighthouse ou WebPageTest pour identifier et résoudre les problèmes de performance.
- Gérer gracieusement les erreurs : Implémentez des frontières d'erreurs pour capturer les erreurs lors de la récupération et du rendu des données, en fournissant des messages d'erreur informatifs aux utilisateurs.
- Envisager le rendu côté serveur (SSR) : Pour des raisons de SEO et de performance, envisagez d'utiliser le SSR avec le streaming et l'hydratation sélective pour offrir une expérience initiale plus rapide.
Conclusion
React Suspense, combiné à des stratégies de récupération de données parallèle, fournit une boîte à outils puissante pour créer des applications réactives et performantes. En comprenant le problème de la cascade et en mettant en œuvre des techniques comme le préchargement, la récupération concurrente avec Promise.all et les hooks personnalisés, vous pouvez améliorer considérablement l'expérience utilisateur. N'oubliez pas de gérer gracieusement les erreurs et de surveiller les performances pour garantir que votre application reste optimisée pour les utilisateurs du monde entier. Alors que React continue d'évoluer, l'exploration de nouvelles fonctionnalités comme l'hydratation sélective avec le rendu serveur en streaming améliorera encore votre capacité à offrir des expériences utilisateur exceptionnelles, quelle que soit la localisation ou les conditions réseau. En adoptant ces techniques, vous pouvez créer des applications qui sont non seulement fonctionnelles, mais aussi un plaisir à utiliser pour votre public mondial.
Ce billet de blog visait à fournir un aperçu complet des stratégies de récupération de données parallèle avec React Suspense. Nous espérons que vous l'avez trouvé informatif et utile. Nous vous encourageons à expérimenter ces techniques dans vos propres projets et à partager vos découvertes avec la communauté.