frontend / react
Data fetching moderne
Explication
Ce que vous allez apprendre
- Identifier les limites du pattern
fetchmanuel dans unuseEffectà l'échelle d'une application - Utiliser TanStack Query pour mettre en cache, dédupliquer et invalider des requêtes
- Comprendre le rôle du primitif
use()pour suspendre un rendu sur une promesse - Implémenter une UI optimiste avec rollback en cas d'échec réel de la requête
- Choisir la bonne approche de data fetching selon la taille et les besoins du projet
Dans quel contexte ?
Sur le tableau de bord src/pages/Dashboard.jsx, le widget "Cours populaires" et le menu latéral affichent tous les deux la liste des cours, chacun avec son propre useEffect + fetch("/api/cours"). Résultat : la même requête part deux fois au chargement de la page, et rien ne rafraîchit ces données après qu'un administrateur publie un nouveau cours ailleurs dans l'application. Migrer vers TanStack Query avec une clé de cache partagée (["cours"]) élimine la requête dupliquée et permet une invalidation centralisée.
Étape 1 : l'approche manuelle fonctionne pour un besoin isolé
Le pattern fetch dans un useEffect, vu plus tôt dans ce cours, fonctionne bien pour un besoin isolé.
Étape 2 : ses limites à l'échelle d'une application
Mais à l'échelle d'une application entière, il révèle vite ses limites : deux composants qui demandent la même donnée la téléchargent deux fois inutilement, et rien ne la rafraîchit automatiquement quand elle devient obsolète.
Étape 3 : la solution, des bibliothèques dédiées
Des outils comme TanStack Query ont été créés pour ce problème : ils introduisent un cache central des données, organisé par "clé".
Étape 4 : comment ce cache fonctionne
Deux composants qui demandent des données avec la même clé partagent automatiquement le résultat en cache. Ce cache peut ensuite être invalidé explicitement après une action qui modifie les données.
Étape 5 : un nouveau primitif, use()
React a introduit use(), un mécanisme permettant de "dérouler" directement une promesse pendant le rendu : tant qu'elle n'est pas résolue, le composant se met en pause.
Étape 6 : une particularité de use()
Contrairement aux autres hooks, use() peut être appelé de façon conditionnelle, ce qui le rend flexible mais réservé à des cas précis.
Une technique complémentaire, l'UI optimiste
Il reste une dernière technique à connaître : mettre à jour l'affichage immédiatement, en anticipant le succès d'une action, puis annuler ce changement si la requête échoue réellement.
Bonne pratique
Pour un "like" ou un ajout aux favoris, l'UI optimiste donne une sensation d'instantanéité précieuse. Prévois systématiquement le chemin de rollback (catch qui restaure l'état précédent) : sans lui, une requête qui échoue laisse l'interface dans un état incohérent avec le serveur.
| Approche | Cache partagé entre composants | Revalidation automatique | Complexité |
|---|---|---|---|
fetch dans useEffect | Non | Non | Faible |
| TanStack Query / SWR | Oui | Oui | Moyenne |
use() + Server Components | Oui (côté serveur) | Selon la stratégie de rendu | Élevée |
Et ensuite ?
Une fois ces approches de data fetching vues, la prochaine étape aborde un sujet essentiel à la fiabilité d'une application : les tests avec React Testing Library.
Commandes & code
Data fetching moderne
Du fetch dans un useEffect jusqu'aux librairies de cache spécialisées et à use().
// Approche historique : fetch manuel dans useEffect -- fonctionne, mais répète beaucoup de logique
function ListeCours() {
const [cours, setCours] = useState([]);
const [chargement, setChargement] = useState(true);
const [erreur, setErreur] = useState(null);
useEffect(() => {
let annule = false;
fetch("/api/cours")
.then((r) => r.json())
.then((data) => { if (!annule) setCours(data); })
.catch((e) => { if (!annule) setErreur(e); })
.finally(() => { if (!annule) setChargement(false); });
return () => { annule = true; };
}, []);
// problèmes non résolus : pas de cache entre composants, pas de revalidation,
// pas de déduplication de requêtes identiques, pas de retry automatique
}// TanStack Query (React Query) : cache, revalidation, déduplication out-of-the-box
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
function ListeCours() {
const { data: cours, isLoading, error } = useQuery({
queryKey: ["cours"], // clé de cache -- deux composants avec la même clé partagent le résultat
queryFn: () => fetch("/api/cours").then((r) => r.json()),
staleTime: 60_000, // considéré "frais" pendant 60s, pas de refetch automatique avant
});
if (isLoading) return <p>Chargement...</p>;
if (error) return <p>Erreur : {error.message}</p>;
return <ul>{cours.map((c) => <li key={c.id}>{c.titre}</li>)}</ul>;
}
function BoutonInscription({ coursId }) {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: () => fetch(`/api/cours/${coursId}/inscription`, { method: "POST" }),
onSuccess: () => {
// invalide le cache -> déclenche un refetch automatique des composants concernés
queryClient.invalidateQueries({ queryKey: ["cours"] });
},
});
return (
<button onClick={() => mutation.mutate()} disabled={mutation.isPending}>
{mutation.isPending ? "Inscription..." : "S'inscrire"}
</button>
);
}// use() : nouveau primitif React pour "dérouler" une Promise (ou lire un Context) pendant le rendu
import { use, Suspense } from "react";
function promesseCours() {
return fetch("/api/cours").then((r) => r.json());
}
function ListeCours({ promesse }) {
const cours = use(promesse); // suspend le rendu tant que la promesse n'est pas résolue
return <ul>{cours.map((c) => <li key={c.id}>{c.titre}</li>)}</ul>;
}
function Page() {
const promesse = promesseCours(); // créée une seule fois, idéalement mémoïsée/venant du serveur
return (
<Suspense fallback={<p>Chargement...</p>}>
<ListeCours promesse={promesse} />
</Suspense>
);
}
// use() peut être appelé conditionnellement (contrairement aux autres hooks) --
// mais reste réservé à des cas précis (Server Components, promesses stables)// Optimistic UI : mettre à jour l'affichage AVANT la confirmation serveur
function BoutonLike({ postId, likesInitial }) {
const [likes, setLikes] = useState(likesInitial);
async function liker() {
setLikes((l) => l + 1); // optimiste : affiché immédiatement
try {
await fetch(`/api/posts/${postId}/like`, { method: "POST" });
} catch {
setLikes((l) => l - 1); // rollback si la requête échoue réellement
}
}
return <button onClick={liker}>+1 ({likes})</button>;
}Résumé
useEffect+fetchmanuel manque de cache, déduplication et revalidation : correct pour un cas isolé, limité à grande échelle.- TanStack Query (ou SWR) centralise le cache par clé et invalide automatiquement les données obsolètes.
use()permet de suspendre un composant sur une promesse directement pendant le rendu, hors des règles classiques des hooks.- L'UI optimiste améliore la perception de réactivité, au prix d'un rollback à gérer en cas d'échec réel.
Exercices pratiques
Mission : réconcilier deux useQuery qui ne se font pas confiance
Objectif : Diagnostiquer un refetch réseau inutile malgré une clé de cache partagée, puis migrer une inscription vers une mutation optimiste avec rollback.
Contexte
Sur Dashboard.jsx, le widget "Cours populaires" appelle useQuery({ queryKey: ["cours"], queryFn, staleTime: 5 * 60_000 }), tandis que le menu latéral appelle useQuery({ queryKey: ["cours"], queryFn }) sans préciser staleTime. Les deux partagent pourtant la même entrée de cache. Au montage de la page, le Profiler réseau montre malgré tout une requête /api/cours qui repart, dix secondes seulement après que le widget a rempli le cache. Il faut aussi migrer BoutonInscription vers une mise à jour optimiste avec rollback propre en cas d'échec réel de l'API.