Tanstack Query
Historiquement, en React, on récupérait les données avec un useEffect.
Le problème du useEffect
Voici un code très classique pour récupérer une liste de produits :
import { useState, useEffect } from 'react';
interface Product {
id: string;
name: string;
price: number;
}
export const ProductList = () => {
const [data, setData] = useState<Product[] | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let isMounted = true; // Pour éviter les fuites de mémoire (Race conditions)
setIsLoading(true);
fetch('/api/products')
.then((res) => {
if (!res.ok) throw new Error("Erreur réseau");
return res.json();
})
.then((json: Product[]) => {
if (isMounted) setData(json);
})
.catch((err) => {
if (isMounted) setError(err instanceof Error ? err : new Error("Erreur inconnue"));
})
.finally(() => {
if (isMounted) setIsLoading(false);
});
return () => {
isMounted = false; // Cleanup function
};
}, []);
if (isLoading) return <div>Chargement...</div>;
if (error) return <div>Erreur : {error.message}</div>;
if (!data) return null;
return <ul>{data.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
};Ce code est lourd, verbeux, et pourtant, il manque l'essentiel : le cache. Si l'utilisateur change de page et revient, la requête repart de zéro. L'utilisateur revoit le spinner de chargement. C'est une très mauvaise expérience utilisateur (UX).
La solution : React Query
TanStack Query (anciennement React Query) est un gestionnaire d'état asynchrone. Il remplace totalement les useEffect de data fetching. Il gère le cache, les requêtes en arrière-plan, les tentatives en cas d'échec (retries), et bien plus.
L'installation et le Provider
Avant de pouvoir utiliser les hooks de React Query, il est nécessaire d'installer la librairie et d'englober notre application dans un QueryClientProvider. Ce provider est le cerveau absolu : c'est lui qui stocke le cache global de toutes vos requêtes.
npm install @tanstack/react-query/* app/providers/QueryProvider.tsx */
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactNode } from 'react';
/* On instancie le client une seule fois (souvent en dehors du composant
ou alors conservé dans un state React) */
const queryClient = new QueryClient();
export const QueryProvider = ({ children }: { children: ReactNode }) => {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
};Il suffit ensuite de wrapper la racine de votre application avec ce provider (typiquement dans votre main.tsx ou l'entrée de l'application). Si vous oubliez le Provider, une belle erreur rouge explosera dans votre console au premier hook appelé
Utilisation dans un composant
Voici exactement le même composant, réécrit avec React Query :
import { useQuery } from '@tanstack/react-query';
interface Product {
id: string;
name: string;
price: number;
}
const fetchProducts = async (): Promise<Product[]> => {
const res = await fetch('/api/products');
if (!res.ok) throw new Error("Erreur réseau");
return res.json();
};
export const ProductList = () => {
/* useQuery gère la donnée, le chargement, les erreurs et le CACHE */
const { data, isLoading, error } = useQuery<Product[], Error>({
queryKey: ['products'], // La clé unique de cette requête dans le cache
queryFn: fetchProducts, // La fonction qui va chercher la donnée
});
if (isLoading) return <div>Chargement...</div>;
if (error) return <div>Erreur : {error.message}</div>;
if (!data) return null;
return <ul>{data.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
};Si l'utilisateur quitte la page et revient, React Query lui affichera instantanément la donnée en cache (0 spinner de chargement), tout en refaisant une requête discrète en arrière-plan pour vérifier si la donnée a changé.
L'API Publique avec FSD
Dans une architecture Feature Sliced Design (FSD) stricte, les composants UI ne devraient pas connaître les clés de cache (['products']). La bonne pratique est d'encapsuler useQuery dans un hook personnalisé situé dans le segment api ou model de votre module.
/* entities/product/api/useProducts.ts */
import { useQuery } from '@tanstack/react-query';
import { fetchProducts } from './productApi';
import { Product } from '../model/types';
export const useProducts = () => {
return useQuery<Product[], Error>({
queryKey: ['products'],
queryFn: fetchProducts,
});
};Suspense et Error Boundaries
Gérer isLoading et isError manuellement dans chaque composant avec des if devient vite pénible quand l'application grossit. La tendance moderne en React est de déléguer cette responsabilité à l'architecture, en utilisant Suspense pour le chargement et Error Boundaries pour les erreurs.
React Query s'intègre parfaitement avec ce modèle :
import { useSuspenseQuery } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';
import { Suspense } from 'react';
/* 1. On utilise useSuspenseQuery au lieu de useQuery.
Ce hook ne retourne pas de isLoading, car React met en pause
le rendu tant que la donnée n'est pas là. */
export const ProductList = () => {
const { data: products } = useSuspenseQuery({
queryKey: ['products'],
queryFn: fetchProducts,
});
return (
<ul>
{products.map((p) => <li key={p.id}>{p.name}</li>)}
</ul>
);
};
/* 2. On encapsule le composant plus haut dans l'arbre */
export const ProductPage = () => {
return (
<ErrorBoundary fallback={<div className="error">Oups, l'API a planté !</div>}>
<Suspense fallback={<div className="spinner">Chargement...</div>}>
<ProductList />
</Suspense>
</ErrorBoundary>
);
};C'est extrêmement puissant : si l'API tombe, ou si vous jetez une erreur manuellement dans votre fetchProducts (par exemple si vous vous rendez compte que le format de données reçu n'est pas le bon), l'ErrorBoundary l'attrape au vol et affiche l'UI de secours. Votre composant ProductList, lui, reste pur et concentré sur l'affichage des données heureuses.
Exercice
J'ai mis en place une API pour cet exercice. Vous pouvez la trouver ici : https://frontend-architecture.teepan.fr/api/products.
On va remplacer les fausses données de notre e-commerce par les vrais produits provenant de l'API du cours. Mais attention : dans le monde réel, les APIs sont parfois "foireuses" (lenteurs, erreurs 500, ou problèmes de typage inattendus).
Dans cet exercice, vous devrez :
- créer un fichier
src/entities/product/api/productApi.tset écrire une fonction asynchronefetchProductsqui fait un appel GET vers l'API. Si!res.ok, levez une erreur explicite. - créer un hook
useProductsqui utiliseuseSuspenseQuery(ouuseQueryavec l'optionsuspensesi vous êtes sur une ancienne version) au lieu duuseQueryclassique. - exporter ce hook dans l'API publique du module produit.
- ouvrir le widget
src/widgets/product-list/ui/ProductList.tsxet appeler votre hook. Le composant gagne en lisibilité puisqu'il n'a plus à gérerisLoading. - le plus important : Dans le parent qui instancie
<ProductList />, wrappez le composant avec un<Suspense>natif de React et un<ErrorBoundary>(vous pouvez l'importer de react-error-boundary ou le coder). Testez de casser volontairement l'URL de l'API pour voir votre UI de fallback s'afficher.
L'architecture de ce projet suit le FSD que nous avons vu dans les chapitres précédents. Vous pouvez repartir de l'état actuel du projet et continuer à travailler sur stackblitz.