La récupération de données dans React a une surface étonnamment grande : états de chargement, états d'erreur, récupération en arrière-plan, invalidation du cache après mutations, mises à jour optimistes, défilement infini, prélecture et déduplication.
Fait partie de notre série Performance et scalabilité
Lire le guide completL'écriture manuelle de cette logique avec useEffect + useState entraîne des conditions de concurrence critique, des fuites de mémoire et des états d'interface utilisateur incohérents. TanStack Query (anciennement React Query) v5 résout tout cela avec une API propre et composable.
Ce guide couvre les modèles TanStack Query v5 pour les applications React de production – des requêtes de base aux flux de mutation complexes, aux mises à jour optimistes, au défilement infini et à l'intégration de Next.js App Router. Chaque modèle inclut TypeScript et la gestion des erreurs.
Points clés à retenir
- TanStack Query est une bibliothèque d'état de serveur — ne l'utilisez pas pour l'état de l'interface utilisateur uniquement client (utilisez Zustand ou useState pour cela)
- Les clés de requête sont l'adresse du cache - incluent toutes les variables qui affectent le résultat de la requête
staleTimecontrôle le moment où la récupération en arrière-plan se déclenche ;gcTimecontrôle quand les données inutilisées sont récupérées- Les mutations devraient invalider les requêtes associées via
queryClient.invalidateQueries()dansonSuccess- Les mises à jour optimistes améliorent les performances perçues mais nécessitent une logique de restauration dans
onError- Utilisez
useInfiniteQuerypour les listes paginées ; ne combinez jamaisuseQuery+ état de la page de manuel- Prélecture au survol pour une sensation de navigation instantanée :
queryClient.prefetchQuery()- Dans Next.js App Router, utilisez
HydrationBoundarypour déshydrater les données récupérées par le serveur dans le client
Installation et configuration
pnpm add @tanstack/react-query @tanstack/react-query-devtools
// src/providers/query-provider.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';
export function QueryProvider({ children }: { children: React.ReactNode }) {
// Create QueryClient inside component to avoid shared state between requests (SSR safety)
const [queryClient] = useState(() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute — data is fresh for 1 minute
gcTime: 5 * 60 * 1000, // 5 minutes — keep unused cache for 5 minutes
retry: 2, // Retry failed requests twice
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30_000), // Exponential backoff
refetchOnWindowFocus: true, // Refetch when user returns to tab
refetchOnReconnect: true, // Refetch after network reconnect
},
mutations: {
retry: 0, // Do not retry mutations (non-idempotent)
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
// src/app/[locale]/layout.tsx
import { QueryProvider } from '@/providers/query-provider';
export default function LocaleLayout({ children }: { children: React.ReactNode }) {
return (
<QueryProvider>
{children}
</QueryProvider>
);
}
Clés de requête — La Fondation
Les clés de requête sont l'adresse du cache de vos données. Chaque variable qui affecte le résultat de la requête doit être dans la clé.
// src/lib/query-keys.ts
// Centralize query keys to avoid typos and enable easy invalidation
export const queryKeys = {
contacts: {
all: () => ['contacts'] as const,
lists: () => [...queryKeys.contacts.all(), 'list'] as const,
list: (filters: ContactFilters) =>
[...queryKeys.contacts.lists(), filters] as const,
details: () => [...queryKeys.contacts.all(), 'detail'] as const,
detail: (id: string) =>
[...queryKeys.contacts.details(), id] as const,
},
orders: {
all: () => ['orders'] as const,
list: (orgId: string, page: number) => ['orders', orgId, page] as const,
detail: (id: string) => ['orders', id] as const,
},
} as const;
// Usage in queries and invalidations:
// useQuery({ queryKey: queryKeys.contacts.list({ page: 1, limit: 20 }) })
// queryClient.invalidateQueries({ queryKey: queryKeys.contacts.lists() })
// — invalidates ALL list queries regardless of filter parameters
Requêtes de base
// src/hooks/use-contacts.ts
import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '@/lib/api';
import { queryKeys } from '@/lib/query-keys';
interface ContactFilters {
page: number;
limit: number;
search?: string;
status?: string;
}
interface ContactsResponse {
data: Contact[];
total: number;
page: number;
limit: number;
}
export function useContacts(filters: ContactFilters) {
return useQuery({
queryKey: queryKeys.contacts.list(filters),
queryFn: async () => {
const params = new URLSearchParams({
page: String(filters.page),
limit: String(filters.limit),
...(filters.search && { search: filters.search }),
...(filters.status && { status: filters.status }),
});
return apiFetch<ContactsResponse>(`/contacts?${params}`);
},
placeholderData: (previousData) => previousData, // Keep previous page data while fetching
staleTime: 30_000, // Contacts data: 30 second stale time
});
}
export function useContact(id: string) {
return useQuery({
queryKey: queryKeys.contacts.detail(id),
queryFn: () => apiFetch<Contact>(`/contacts/${id}`),
enabled: !!id, // Only fetch if id is provided
});
}
// Usage in a component
'use client';
export default function ContactsPage() {
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
const { data, isLoading, isError, error, isFetching } = useContacts({
page,
limit: 20,
search: search || undefined,
});
if (isLoading) return <ContactsSkeleton />;
if (isError) return <ErrorState message={error.message} />;
return (
<div>
{isFetching && <RefetchIndicator />} {/* Shows during background refetch */}
<ContactsTable data={data.data} />
<Pagination
page={page}
total={data.total}
limit={20}
onChange={setPage}
/>
</div>
);
}
Mutations avec invalidation du cache
// src/hooks/use-contact-mutations.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { apiFetch } from '@/lib/api';
import { queryKeys } from '@/lib/query-keys';
import { toast } from '@/components/ui/toast';
export function useCreateContact() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateContactDto) =>
apiFetch<Contact>('/contacts', {
method: 'POST',
body: JSON.stringify(data),
}),
onSuccess: (newContact) => {
// Invalidate all contact list queries — they are now stale
queryClient.invalidateQueries({
queryKey: queryKeys.contacts.lists(),
});
// Seed the detail cache immediately — no need to refetch
queryClient.setQueryData(
queryKeys.contacts.detail(newContact.id),
newContact
);
toast({ title: 'Contact created', variant: 'success' });
},
onError: (error: Error) => {
toast({ title: 'Failed to create contact', description: error.message, variant: 'destructive' });
},
});
}
export function useDeleteContact() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) =>
apiFetch(`/contacts/${id}`, { method: 'DELETE' }),
onSuccess: (_data, deletedId) => {
// Remove from all list queries
queryClient.invalidateQueries({ queryKey: queryKeys.contacts.lists() });
// Remove detail cache
queryClient.removeQueries({ queryKey: queryKeys.contacts.detail(deletedId) });
toast({ title: 'Contact deleted', variant: 'success' });
},
});
}
Mises à jour optimistes
Les mises à jour optimistes rendent les mutations instantanées en mettant à jour l'interface utilisateur avant que le serveur ne confirme :
export function useUpdateContact() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdateContactDto }) =>
apiFetch<Contact>(`/contacts/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
}),
// Called immediately before mutationFn
onMutate: async ({ id, data }) => {
// Cancel any in-flight refetches for this contact
await queryClient.cancelQueries({ queryKey: queryKeys.contacts.detail(id) });
// Snapshot the previous value for rollback
const previousContact = queryClient.getQueryData<Contact>(
queryKeys.contacts.detail(id)
);
// Optimistically update the UI
queryClient.setQueryData<Contact>(
queryKeys.contacts.detail(id),
(old) => old ? { ...old, ...data } : old
);
return { previousContact }; // Passed to onError context
},
onError: (_error, { id }, context) => {
// Roll back to the snapshot on error
if (context?.previousContact) {
queryClient.setQueryData(
queryKeys.contacts.detail(id),
context.previousContact
);
}
toast({ title: 'Update failed', variant: 'destructive' });
},
onSettled: (_data, _error, { id }) => {
// Always refetch after success or error to sync with server truth
queryClient.invalidateQueries({ queryKey: queryKeys.contacts.detail(id) });
},
});
}
Défilement infini / Pagination
// src/hooks/use-infinite-contacts.ts
import { useInfiniteQuery } from '@tanstack/react-query';
export function useInfiniteContacts(search?: string) {
return useInfiniteQuery({
queryKey: ['contacts', 'infinite', search],
queryFn: ({ pageParam = 1 }) =>
apiFetch<ContactsResponse>(`/contacts?page=${pageParam}&limit=20${search ? `&search=${search}` : ''}`),
initialPageParam: 1,
getNextPageParam: (lastPage) => {
const nextPage = lastPage.page + 1;
const maxPage = Math.ceil(lastPage.total / lastPage.limit);
return nextPage <= maxPage ? nextPage : undefined;
},
staleTime: 30_000,
});
}
// Infinite scroll component
'use client';
import { useInfiniteContacts } from '@/hooks/use-infinite-contacts';
import { useInView } from 'react-intersection-observer';
import { useEffect } from 'react';
export function InfiniteContactsList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
} = useInfiniteContacts();
const { ref, inView } = useInView({ threshold: 0.5 });
// Fetch next page when the sentinel element enters the viewport
useEffect(() => {
if (inView && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
if (isLoading) return <ContactsSkeleton />;
const contacts = data?.pages.flatMap((page) => page.data) ?? [];
return (
<div>
{contacts.map((contact) => (
<ContactCard key={contact.id} contact={contact} />
))}
{/* Sentinel element — triggers next page load when visible */}
<div ref={ref} className="h-4">
{isFetchingNextPage && <LoadingSpinner />}
</div>
</div>
);
}
Prélecture pour la navigation instantanée
// Prefetch on hover — data is ready before the user clicks
'use client';
import { useQueryClient } from '@tanstack/react-query';
import { Link } from '@/i18n/navigation';
import { queryKeys } from '@/lib/query-keys';
import { apiFetch } from '@/lib/api';
export function ContactRow({ contact }: { contact: Contact }) {
const queryClient = useQueryClient();
const handleMouseEnter = () => {
queryClient.prefetchQuery({
queryKey: queryKeys.contacts.detail(contact.id),
queryFn: () => apiFetch<Contact>(`/contacts/${contact.id}`),
staleTime: 60_000, // Use cached if less than 1 minute old
});
};
return (
<tr onMouseEnter={handleMouseEnter}>
<td>
<Link href={`/dashboard/contacts/${contact.id}`}>{contact.name}</Link>
</td>
<td>{contact.email}</td>
</tr>
);
}
Next.js App Router SSR + Requête TanStack
// src/app/[locale]/dashboard/contacts/page.tsx (Server Component)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import { queryKeys } from '@/lib/query-keys';
import { getServerContacts } from '@/lib/server-api';
import { ContactsClient } from './contacts-client';
export default async function ContactsPage({ params }: Props) {
const { locale } = await params;
const queryClient = new QueryClient();
// Prefetch on the server — data is embedded in the HTML
await queryClient.prefetchQuery({
queryKey: queryKeys.contacts.list({ page: 1, limit: 20 }),
queryFn: () => getServerContacts({ page: 1, limit: 20 }),
});
return (
// HydrationBoundary serializes the server QueryClient state
// into the HTML, which the client QueryClient then picks up
<HydrationBoundary state={dehydrate(queryClient)}>
<ContactsClient />
</HydrationBoundary>
);
}
// src/app/[locale]/dashboard/contacts/contacts-client.tsx ('use client')
'use client';
import { useContacts } from '@/hooks/use-contacts';
export function ContactsClient() {
const { data } = useContacts({ page: 1, limit: 20 });
// data is immediately available from the server-dehydrated state
// No loading state on first render — SSR data is used instantly
return <ContactsTable data={data?.data ?? []} />;
}
Questions fréquemment posées
Quand dois-je utiliser TanStack Query vs SWR vs une simple récupération ?
Utilisez TanStack Query lorsque vous en avez besoin : invalidation du cache après des mutations, mises à jour optimistes, défilement infini, prélecture, récupération en arrière-plan ou demande de déduplication. Utilisez SWR si vous avez besoin d’un bundle plus petit et uniquement de cas d’utilisation simples. Utilisez simplement fetch dans les composants serveur Next.js où vous souhaitez la mise en cache des données natives de React (la fonction cache() et le modèle revalidatePath()). TanStack Query est destiné à l'interactivité côté client où les actions de l'utilisateur affectent les données du serveur.
Quelle est la différence entre staleTime et gcTime ?
staleTime correspond à la durée pendant laquelle les données sont considérées comme fraîches : pendant cette période, aucune récupération en arrière-plan n'a lieu et les données mises en cache sont renvoyées immédiatement. Après l'expiration de staleTime, les données sont « périmées » et seront récupérées en arrière-plan lors de la prochaine utilisation. gcTime (anciennement cacheTime) correspond à la durée pendant laquelle les données inutilisées (aucun abonné actif) restent en mémoire avant le garbage collection. Définissez staleTime en fonction de la rapidité avec laquelle vos données changent ; définissez gcTime en fonction de la durée pendant laquelle vous souhaitez que la navigation aller et retour soit instantanée.
Comment gérer les erreurs d'authentification (401) de manière globale ?
Ajoutez un gestionnaire d'erreurs global dans vos options par défaut QueryClient. Sur 401, redirigez pour vous connecter et videz le cache des requêtes. Dans TanStack Query v5 : defaultOptions: { queries: { throwOnError: (error) => { if (error.status === 401) { router.push('/auth/login'); queryClient.clear(); return false; } return true; } } }. Vous pouvez également gérer 401 dans votre assistant apiFetch en appelant une fonction logout().
Comment partager l'état de la requête entre des composants frères sans perçage d'accessoires ?
Tout composant qui appelle useQuery avec la même clé de requête partage les mêmes données mises en cache — TanStack Query déduplique automatiquement les requêtes. Deux composants ContactsTable et un badge ContactCount appelant useContacts({ page: 1, limit: 20 }) avec les mêmes filtres seront tous lus à partir d'une seule requête mise en cache. Pas de forage d'accessoires, aucun contexte nécessaire.
Les clés de requête doivent-elles inclure l'ID de session ou d'organisation de l'utilisateur ?
Oui : si les données sont limitées à un utilisateur ou à une organisation, incluez l'identifiant de portée dans la clé de requête. Cela empêche les données d'un utilisateur d'apparaître pour un autre (critique pour les applications multi-locataires). Effacez également l'intégralité du cache de requêtes à la déconnexion : queryClient.clear() ou queryClient.removeQueries() — sinon les données obsolètes de la session précédente peuvent clignoter brièvement pour l'utilisateur suivant.
Prochaines étapes
TanStack Query transforme la récupération de données React d'un problème de gestion d'état en un système déclaratif prenant en compte le cache. Les mises à jour optimistes, la prélecture et l'hydratation SSR offrent à vos utilisateurs une expérience instantanée, même sur des connexions lentes.
ECOSIRE crée des panneaux d'administration Next.js et des portails clients avec TanStack Query pour tous les états du serveur, combinés avec Zustand pour l'état de l'interface utilisateur uniquement client et TanStack Table pour les vues gourmandes en données. Découvrez nos services d'ingénierie front-end pour découvrir comment nous créons des applications React performantes et prêtes pour la production.
Rédigé par
ECOSIRE Research and Development TeamRédaction technique
L'équipe de rédaction technique d'ECOSIRE traite d'Odoo ERP, de l'e-commerce avec Shopify, des agents IA, de l'analytique avec Power BI, de l'automatisation avec GoHighLevel et des bonnes pratiques du logiciel d'entreprise. Nos guides aident les entreprises à prendre des décisions technologiques éclairées.
ECOSIRE
Développez votre entreprise avec ECOSIRE
Solutions d'entreprise pour l'ERP, le commerce électronique, l'IA, l'analyse et l'automatisation.
Articles connexes
Routeur d'application Next.js 16 : modèles de production et pièges
Modèles de routeur d'applications Next.js 16 prêts pour la production : composants de serveur, stratégies de mise en cache, API de métadonnées, limites d'erreur et pièges de performances à éviter.
Configuration de production Nginx : SSL, mise en cache et sécurité
Guide de configuration de production Nginx : terminaison SSL, HTTP/2, en-têtes de mise en cache, en-têtes de sécurité, limitation de débit, configuration du proxy inverse et modèles d'intégration Cloudflare.
Optimisation des coûts des agents IA : utilisation des jetons et mise en cache
Stratégies pratiques pour réduire les coûts opérationnels des agents d'IA grâce à l'optimisation des jetons, à la mise en cache, au routage des modèles et à la surveillance de l'utilisation. De réelles économies grâce aux déploiements de production OpenClaw.
Plus de Performance et scalabilité
Débogage et surveillance des webhooks : le guide de dépannage complet
Maîtrisez le débogage des webhooks avec ce guide complet couvrant les modèles de défaillance, les outils de débogage, les stratégies de nouvelle tentative, les tableaux de bord de surveillance et les meilleures pratiques de sécurité.
Tests de charge k6 : testez sous contrainte vos API avant le lancement
Maîtrisez les tests de charge K6 pour les API Node.js. Couvre les montées en puissance des utilisateurs virtuels, les seuils, les scénarios, HTTP/2, les tests WebSocket, les tableaux de bord Grafana et les modèles d'intégration CI.
Configuration de production Nginx : SSL, mise en cache et sécurité
Guide de configuration de production Nginx : terminaison SSL, HTTP/2, en-têtes de mise en cache, en-têtes de sécurité, limitation de débit, configuration du proxy inverse et modèles d'intégration Cloudflare.
Odoo Performance Tuning : PostgreSQL et optimisation du serveur
Guide expert sur le réglage des performances d’Odoo 19. Couvre la configuration PostgreSQL, l'indexation, l'optimisation des requêtes, la mise en cache Nginx et le dimensionnement du serveur pour les déploiements d'entreprise.
Test et surveillance des agents IA en production
Un guide complet pour tester et surveiller les agents IA dans les environnements de production. Couvre les cadres d'évaluation, l'observabilité, la détection des dérives et la réponse aux incidents pour les déploiements OpenClaw.
Agents de surveillance de la conformité avec OpenClaw
Déployez des agents OpenClaw AI pour une surveillance continue de la conformité. Automatisez les contrôles réglementaires, l’application des politiques, la génération de pistes d’audit et les rapports de conformité.