Retour au cours

frontend / react

Data fetching moderne

Leçon 181 exercice

Explication

Ce que vous allez apprendre

  • Identifier les limites du pattern fetch manuel dans un useEffect à 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.

ApprocheCache partagé entre composantsRevalidation automatiqueComplexité
fetch dans useEffectNonNonFaible
TanStack Query / SWROuiOuiMoyenne
use() + Server ComponentsOui (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().

jsx
// 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
}
jsx
// 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>
    );
}
jsx
// 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)
jsx
// 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 + fetch manuel 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

1 disponible
1

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.

Résoudre l’exercice →