TanStack Query v5 avec Next.js App Router : le guide complet du data fetching

TanStack Query v5 est la référence pour la gestion du server state en React. Combiné avec le Next.js App Router, il crée une couche de données puissante qui gère le caching, le refetching en arrière-plan, les mises à jour optimistes et bien plus — le tout avec un minimum de boilerplate. Dans ce tutoriel, vous construirez une couche de données prête pour la production.
Ce que vous allez construire
Une application BookShelf — un gestionnaire de collection de livres incluant :
- Prefetching côté serveur avec Next.js App Router
- Caching et refetching en arrière-plan côté client
- Mutations optimistes pour des mises à jour instantanées de l'interface
- Pagination par scroll infini
- Requêtes dépendantes et fetching parallèle
- Error boundaries et états de chargement
- Intégration des devtools pour le débogage
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé
- Des connaissances de base en React et TypeScript
- Une familiarité avec Next.js App Router (layouts, pages, server components)
- Un éditeur de code (VS Code recommandé)
- Une compréhension basique des APIs REST
Étape 1 : Configuration du projet
Créez un nouveau projet Next.js avec TypeScript :
npx create-next-app@latest bookshelf --typescript --tailwind --eslint --app --src-dir
cd bookshelfInstallez TanStack Query v5 et les dépendances associées :
npm install @tanstack/react-query @tanstack/react-query-devtools
npm install zodLa structure de votre projet devrait ressembler à ceci :
bookshelf/
├── src/
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── books/
│ ├── lib/
│ │ ├── query-client.ts
│ │ └── api.ts
│ └── components/
│ └── providers.tsx
├── package.json
└── tsconfig.json
Étape 2 : Configurer le Query Client
Le Query Client est le cœur de TanStack Query. Il gère tous les caches de requêtes, les valeurs par défaut et le comportement de refetching en arrière-plan.
Créez src/lib/query-client.ts :
import { QueryClient, defaultShouldDehydrateQuery, isServer } from "@tanstack/react-query";
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
gcTime: 5 * 60 * 1000, // 5 minutes de garbage collection
retry: 2,
refetchOnWindowFocus: false,
},
dehydrate: {
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) ||
query.state.status === "pending",
},
},
});
}
let browserQueryClient: QueryClient | undefined = undefined;
export function getQueryClient() {
if (isServer) {
// Serveur : toujours créer un nouveau query client
return makeQueryClient();
} else {
// Navigateur : créer un nouveau query client seulement s'il n'existe pas
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
}Pourquoi séparer les clients serveur et navigateur ? Sur le serveur, chaque requête doit obtenir un QueryClient frais pour empêcher les fuites de données entre utilisateurs. Sur le navigateur, on réutilise le même client pour que le cache persiste entre les navigations.
Étape 3 : Créer le composant Providers
TanStack Query nécessite un provider qui enveloppe votre application. Créez src/components/providers.tsx :
"use client";
import { QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { getQueryClient } from "@/lib/query-client";
export function Providers({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient();
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}Mettez à jour src/app/layout.tsx pour utiliser le provider :
import type { Metadata } from "next";
import { Providers } from "@/components/providers";
import "./globals.css";
export const metadata: Metadata = {
title: "BookShelf - Démo TanStack Query",
description: "Un gestionnaire de collection de livres construit avec TanStack Query v5",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="fr">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Étape 4 : Définir les types et la couche API
Créez les définitions de types et les fonctions API. D'abord, créez src/lib/types.ts :
import { z } from "zod";
export const BookSchema = z.object({
id: z.string(),
title: z.string(),
author: z.string(),
coverUrl: z.string().url(),
description: z.string(),
genre: z.string(),
rating: z.number().min(0).max(5),
publishedYear: z.number(),
createdAt: z.string().datetime(),
});
export type Book = z.infer<typeof BookSchema>;
export const BooksResponseSchema = z.object({
books: z.array(BookSchema),
nextCursor: z.string().nullable(),
totalCount: z.number(),
});
export type BooksResponse = z.infer<typeof BooksResponseSchema>;Ensuite, créez la couche API dans src/lib/api.ts :
import type { Book, BooksResponse } from "./types";
const API_BASE = "/api";
export async function fetchBooks(params: {
cursor?: string;
limit?: number;
genre?: string;
search?: string;
}): Promise<BooksResponse> {
const searchParams = new URLSearchParams();
if (params.cursor) searchParams.set("cursor", params.cursor);
if (params.limit) searchParams.set("limit", String(params.limit));
if (params.genre) searchParams.set("genre", params.genre);
if (params.search) searchParams.set("search", params.search);
const res = await fetch(`${API_BASE}/books?${searchParams}`);
if (!res.ok) throw new Error("Échec du chargement des livres");
return res.json();
}
export async function fetchBook(id: string): Promise<Book> {
const res = await fetch(`${API_BASE}/books/${id}`);
if (!res.ok) throw new Error("Livre non trouvé");
return res.json();
}
export async function createBook(
data: Omit<Book, "id" | "createdAt">
): Promise<Book> {
const res = await fetch(`${API_BASE}/books`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error("Échec de la création du livre");
return res.json();
}
export async function updateBook(
id: string,
data: Partial<Omit<Book, "id" | "createdAt">>
): Promise<Book> {
const res = await fetch(`${API_BASE}/books/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error("Échec de la mise à jour du livre");
return res.json();
}
export async function deleteBook(id: string): Promise<void> {
const res = await fetch(`${API_BASE}/books/${id}`, {
method: "DELETE",
});
if (!res.ok) throw new Error("Échec de la suppression du livre");
}Étape 5 : Créer les factories de query keys
L'un des patterns les plus importants dans TanStack Query est l'organisation de vos clés de requête. Créez src/lib/queries.ts :
import { queryOptions, infiniteQueryOptions } from "@tanstack/react-query";
import { fetchBooks, fetchBook } from "./api";
export const bookKeys = {
all: ["books"] as const,
lists: () => [...bookKeys.all, "list"] as const,
list: (filters: { genre?: string; search?: string }) =>
[...bookKeys.lists(), filters] as const,
details: () => [...bookKeys.all, "detail"] as const,
detail: (id: string) => [...bookKeys.details(), id] as const,
};
// Options de requête pour un livre unique
export function bookQueryOptions(id: string) {
return queryOptions({
queryKey: bookKeys.detail(id),
queryFn: () => fetchBook(id),
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
// Options de requête infinie pour la liste de livres
export function booksInfiniteQueryOptions(filters: {
genre?: string;
search?: string;
}) {
return infiniteQueryOptions({
queryKey: bookKeys.list(filters),
queryFn: ({ pageParam }) =>
fetchBooks({
cursor: pageParam,
limit: 12,
...filters,
}),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
}Les factories de query keys sont un pattern recommandé par l'équipe TanStack. Elles centralisent toutes vos clés de requête en un seul endroit, rendant l'invalidation du cache prévisible et type-safe. La structure hiérarchique permet d'invalider à n'importe quel niveau — tous les livres, toutes les listes, ou un détail spécifique.
Étape 6 : Prefetching côté serveur
C'est ici que la combinaison Next.js App Router et TanStack Query brille vraiment. Vous pouvez précharger les données sur le serveur et les transmettre au client sans cascades de requêtes.
Créez src/app/books/page.tsx :
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import { getQueryClient } from "@/lib/query-client";
import { booksInfiniteQueryOptions } from "@/lib/queries";
import { BookList } from "@/components/book-list";
interface BooksPageProps {
searchParams: Promise<{ genre?: string; search?: string }>;
}
export default async function BooksPage({ searchParams }: BooksPageProps) {
const params = await searchParams;
const queryClient = getQueryClient();
// Prefetch sur le serveur — données immédiatement disponibles sur le client
await queryClient.prefetchInfiniteQuery(
booksInfiniteQueryOptions({
genre: params.genre,
search: params.search,
})
);
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">Ma Bibliothèque</h1>
<BookList
initialGenre={params.genre}
initialSearch={params.search}
/>
</div>
</HydrationBoundary>
);
}Pour les pages de livres individuels, créez src/app/books/[id]/page.tsx :
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import { getQueryClient } from "@/lib/query-client";
import { bookQueryOptions } from "@/lib/queries";
import { BookDetail } from "@/components/book-detail";
interface BookPageProps {
params: Promise<{ id: string }>;
}
export default async function BookPage({ params }: BookPageProps) {
const { id } = await params;
const queryClient = getQueryClient();
await queryClient.prefetchQuery(bookQueryOptions(id));
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<BookDetail id={id} />
</HydrationBoundary>
);
}Étape 7 : Construire la liste de livres avec scroll infini
Créez src/components/book-list.tsx :
"use client";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useInView } from "react-intersection-observer";
import { useEffect, useState } from "react";
import { booksInfiniteQueryOptions } from "@/lib/queries";
import { BookCard } from "./book-card";
interface BookListProps {
initialGenre?: string;
initialSearch?: string;
}
export function BookList({ initialGenre, initialSearch }: BookListProps) {
const [genre, setGenre] = useState(initialGenre);
const [search, setSearch] = useState(initialSearch ?? "");
const { ref, inView } = useInView();
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
isError,
error,
} = useInfiniteQuery(booksInfiniteQueryOptions({ genre, search }));
// Charger automatiquement la page suivante quand la sentinelle est visible
useEffect(() => {
if (inView && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
const books = data?.pages.flatMap((page) => page.books) ?? [];
const totalCount = data?.pages[0]?.totalCount ?? 0;
if (isError) {
return (
<div className="text-red-500 p-4 rounded-lg bg-red-50">
Erreur de chargement : {error.message}
</div>
);
}
return (
<div>
{/* Barre de recherche et filtres */}
<div className="flex gap-4 mb-6">
<input
type="text"
placeholder="Rechercher des livres..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="flex-1 px-4 py-2 border rounded-lg"
/>
<select
value={genre ?? ""}
onChange={(e) => setGenre(e.target.value || undefined)}
className="px-4 py-2 border rounded-lg"
>
<option value="">Tous les genres</option>
<option value="fiction">Fiction</option>
<option value="non-fiction">Non-fiction</option>
<option value="sci-fi">Science-fiction</option>
<option value="biography">Biographie</option>
</select>
</div>
{/* Compteur de résultats */}
<p className="text-sm text-gray-500 mb-4">
Affichage de {books.length} sur {totalCount} livres
</p>
{/* Grille de livres */}
{isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="animate-pulse bg-gray-200 rounded-lg h-64" />
))}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{books.map((book) => (
<BookCard key={book.id} book={book} />
))}
</div>
)}
{/* Sentinelle du scroll infini */}
<div ref={ref} className="h-10 mt-4">
{isFetchingNextPage && (
<p className="text-center text-gray-500">Chargement en cours...</p>
)}
</div>
</div>
);
}Étape 8 : Mutations optimistes
Les mises à jour optimistes rendent votre application instantanée en mettant à jour l'interface avant la réponse du serveur. Créez src/hooks/use-book-mutations.ts :
"use client";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { createBook, updateBook, deleteBook } from "@/lib/api";
import { bookKeys } from "@/lib/queries";
import type { Book } from "@/lib/types";
export function useCreateBook() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createBook,
onSuccess: () => {
// Invalider toutes les listes pour refetch avec le nouveau livre
queryClient.invalidateQueries({ queryKey: bookKeys.lists() });
},
});
}
export function useUpdateBook(id: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: Partial<Omit<Book, "id" | "createdAt">>) =>
updateBook(id, data),
// Mise à jour optimiste
onMutate: async (newData) => {
// Annuler les refetch en cours
await queryClient.cancelQueries({ queryKey: bookKeys.detail(id) });
// Sauvegarder un snapshot de la valeur précédente
const previousBook = queryClient.getQueryData(bookKeys.detail(id));
// Mettre à jour le cache de manière optimiste
queryClient.setQueryData(bookKeys.detail(id), (old: Book | undefined) =>
old ? { ...old, ...newData } : old
);
return { previousBook };
},
// En cas d'échec, revenir à la valeur précédente
onError: (_err, _newData, context) => {
if (context?.previousBook) {
queryClient.setQueryData(bookKeys.detail(id), context.previousBook);
}
},
// Toujours refetch après erreur ou succès
onSettled: () => {
queryClient.invalidateQueries({ queryKey: bookKeys.detail(id) });
queryClient.invalidateQueries({ queryKey: bookKeys.lists() });
},
});
}
export function useDeleteBook() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: deleteBook,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: bookKeys.lists() });
},
});
}Le pattern de mise à jour optimiste suit trois étapes : (1) annuler les requêtes sortantes, (2) sauvegarder un snapshot des anciennes données et appliquer la mise à jour immédiatement, (3) rollback en cas d'erreur. Cela donne aux utilisateurs un retour instantané tout en maintenant la cohérence des données.
Étape 9 : Requêtes dépendantes et parallèles
Parfois, vous devez récupérer des données qui dépendent d'autres données. Créez src/hooks/use-book-with-reviews.ts :
"use client";
import { useQuery, useQueries } from "@tanstack/react-query";
import { bookQueryOptions } from "@/lib/queries";
async function fetchReviews(bookId: string) {
const res = await fetch(`/api/books/${bookId}/reviews`);
if (!res.ok) throw new Error("Échec du chargement des avis");
return res.json();
}
async function fetchRelatedBooks(genre: string) {
const res = await fetch(`/api/books?genre=${genre}&limit=4`);
if (!res.ok) throw new Error("Échec du chargement des livres similaires");
return res.json();
}
// Requête dépendante : les avis se chargent après les données du livre
export function useBookWithReviews(bookId: string) {
const bookQuery = useQuery(bookQueryOptions(bookId));
const reviewsQuery = useQuery({
queryKey: ["books", bookId, "reviews"],
queryFn: () => fetchReviews(bookId),
// Ne charger les avis que quand les données du livre sont disponibles
enabled: !!bookQuery.data,
});
return { bookQuery, reviewsQuery };
}
// Requêtes parallèles : récupérer plusieurs ressources indépendantes simultanément
export function useBookPageData(bookId: string) {
const results = useQueries({
queries: [
bookQueryOptions(bookId),
{
queryKey: ["books", bookId, "reviews"],
queryFn: () => fetchReviews(bookId),
},
],
});
return {
book: results[0],
reviews: results[1],
};
}Étape 10 : Recherche avec requêtes différées
Pour la fonctionnalité de recherche, vous voulez différer les appels API. Créez src/hooks/use-debounced-search.ts :
"use client";
import { useQuery } from "@tanstack/react-query";
import { useState, useDeferredValue } from "react";
import { fetchBooks } from "@/lib/api";
export function useDebouncedBookSearch() {
const [search, setSearch] = useState("");
const deferredSearch = useDeferredValue(search);
const query = useQuery({
queryKey: ["books", "search", deferredSearch],
queryFn: () => fetchBooks({ search: deferredSearch, limit: 10 }),
enabled: deferredSearch.length >= 2,
placeholderData: (previousData) => previousData, // Garder les anciennes données visibles
});
return {
search,
setSearch,
isStale: search !== deferredSearch,
...query,
};
}Cela utilise le useDeferredValue de React 19 au lieu d'un timer de debounce manuel. L'option placeholderData garde les résultats précédents visibles pendant le chargement, évitant les sauts de mise en page.
Étape 11 : Gestion des erreurs et stratégies de retry
Créez une error boundary robuste. Créez src/components/query-error-boundary.tsx :
"use client";
import { QueryErrorResetBoundary } from "@tanstack/react-query";
import { ErrorBoundary } from "react-error-boundary";
interface QueryErrorBoundaryProps {
children: React.ReactNode;
}
export function QueryErrorBoundary({ children }: QueryErrorBoundaryProps) {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={({ resetErrorBoundary, error }) => (
<div className="p-6 rounded-lg bg-red-50 border border-red-200">
<h3 className="text-lg font-semibold text-red-800 mb-2">
Une erreur est survenue
</h3>
<p className="text-red-600 mb-4">{error.message}</p>
<button
onClick={resetErrorBoundary}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
>
Réessayer
</button>
</div>
)}
>
{children}
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
);
}Vous pouvez personnaliser le comportement de retry par requête :
useQuery({
queryKey: ["critical-data"],
queryFn: fetchCriticalData,
retry: (failureCount, error) => {
// Ne pas réessayer sur les 404
if (error instanceof Error && error.message.includes("not found")) {
return false;
}
// Réessayer jusqu'à 3 fois pour les autres erreurs
return failureCount < 3;
},
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
});Étape 12 : Patterns d'invalidation de requêtes
Comprendre quand et comment invalider les requêtes est crucial. Voici les patterns clés :
const queryClient = useQueryClient();
// 1. Invalider tout
queryClient.invalidateQueries();
// 2. Invalider tous les livres (listes + détails)
queryClient.invalidateQueries({ queryKey: bookKeys.all });
// 3. Invalider uniquement les listes de livres
queryClient.invalidateQueries({ queryKey: bookKeys.lists() });
// 4. Invalider une liste spécifique avec filtres
queryClient.invalidateQueries({
queryKey: bookKeys.list({ genre: "fiction" }),
});
// 5. Invalider le détail d'un seul livre
queryClient.invalidateQueries({ queryKey: bookKeys.detail("book-123") });
// 6. Supprimer une requête du cache entièrement
queryClient.removeQueries({ queryKey: bookKeys.detail("deleted-book") });
// 7. Précharger des données pour une navigation probable
queryClient.prefetchQuery(bookQueryOptions("likely-next-book-id"));Astuce pro : Utilisez invalidateQueries dans la plupart des cas — cela marque les données comme périmées et refetch en arrière-plan. Utilisez removeQueries uniquement quand les données ne devraient plus exister (comme après la suppression d'une ressource). Utilisez setQueryData pour les mises à jour optimistes où vous connaissez la nouvelle valeur.
Étape 13 : Route handlers API
Créez les routes API pour alimenter votre application. Créez src/app/api/books/route.ts :
import { NextRequest, NextResponse } from "next/server";
// Store en mémoire pour la démo
const books = new Map<string, any>();
// Initialiser quelques données
const genres = ["fiction", "non-fiction", "sci-fi", "biography"];
for (let i = 1; i <= 50; i++) {
const id = `book-${i}`;
books.set(id, {
id,
title: `Titre du livre ${i}`,
author: `Auteur ${i}`,
coverUrl: `https://picsum.photos/seed/${id}/300/400`,
description: `Un livre fascinant sur le sujet ${i}.`,
genre: genres[i % genres.length],
rating: Math.round((Math.random() * 4 + 1) * 10) / 10,
publishedYear: 2020 + (i % 7),
createdAt: new Date().toISOString(),
});
}
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const cursor = searchParams.get("cursor");
const limit = parseInt(searchParams.get("limit") ?? "12");
const genre = searchParams.get("genre");
const search = searchParams.get("search");
let allBooks = Array.from(books.values());
if (genre) {
allBooks = allBooks.filter((b) => b.genre === genre);
}
if (search) {
const lowerSearch = search.toLowerCase();
allBooks = allBooks.filter(
(b) =>
b.title.toLowerCase().includes(lowerSearch) ||
b.author.toLowerCase().includes(lowerSearch)
);
}
const startIndex = cursor
? allBooks.findIndex((b) => b.id === cursor) + 1
: 0;
const paginated = allBooks.slice(startIndex, startIndex + limit);
const nextCursor =
startIndex + limit < allBooks.length
? allBooks[startIndex + limit - 1]?.id
: null;
return NextResponse.json({
books: paginated,
nextCursor,
totalCount: allBooks.length,
});
}
export async function POST(request: NextRequest) {
const data = await request.json();
const id = `book-${Date.now()}`;
const book = {
...data,
id,
createdAt: new Date().toISOString(),
};
books.set(id, book);
return NextResponse.json(book, { status: 201 });
}Tester votre implémentation
Lancez le serveur de développement :
npm run devOuvrez les TanStack Query Devtools (l'icône flottante en bas à droite) pour inspecter :
- Requêtes actives — Voir toutes les requêtes en cours et leur état
- Entrées du cache — Inspecter les données mises en cache
- Timeline des requêtes — Observer les patterns de fetch/refetch
- Indicateurs stale/fresh — Comprendre vos paramètres staleTime
Testez les scénarios suivants :
- Naviguer entre les pages — Remarquez comment les données sont cachées et instantanément disponibles au retour
- Filtrer par genre — Observez les nouvelles requêtes se lancer tandis que les résultats en cache restent disponibles
- Modifier un livre — Voyez la mise à jour optimiste se produire instantanément, puis la confirmation du serveur
- Passer hors ligne — Les données en cache restent disponibles, les mutations sont mises en file d'attente
Dépannage
Problèmes courants et solutions :
Erreurs de mismatch d'hydratation
Assurez-vous que getQueryClient() retourne le même client sur le navigateur mais un nouveau sur le serveur. Le pattern de l'étape 2 gère cela correctement.
Requêtes qui refetch trop souvent
Augmentez le staleTime dans vos options de requête. La valeur 0 (par défaut) signifie que les données sont immédiatement périmées et seront refetchées à chaque montage de composant.
Le scroll infini ne fonctionne pas
Assurez-vous que getNextPageParam retourne undefined (pas null) quand il n'y a plus de pages. TanStack Query vérifie spécifiquement undefined.
Les données ne se mettent pas à jour après une mutation
Assurez-vous que votre appel invalidateQueries utilise le bon scope de query key. Utilisez le pattern de factory de query keys pour garder les clés cohérentes.
Prochaines étapes
Maintenant que vous avez une base solide avec TanStack Query v5 et Next.js :
- Ajoutez le support Suspense — Utilisez
useSuspenseQuerypour un pattern de chargement plus déclaratif - Implémentez le support hors-ligne — Utilisez
@tanstack/query-persist-client-corepour persister le cache dans localStorage - Ajoutez des abonnements WebSocket — Combinez TanStack Query avec des sources de données temps réel
- Explorez l'annulation de requêtes — Utilisez
AbortSignalpour annuler les requêtes en cours - Construisez une file de mutations — Gérez les mutations hors-ligne qui se synchronisent au retour de la connexion
Conclusion
TanStack Query v5 transforme la façon dont vous gérez le server state dans les applications Next.js. En combinant le prefetching côté serveur avec le caching côté client, vous obtenez le meilleur des deux mondes : des chargements initiaux rapides et des navigations suivantes instantanées. Le pattern de factory de query keys rend l'invalidation du cache prévisible, tandis que les mutations optimistes rendent votre interface réactive.
Les points clés à retenir :
- Séparez les query clients serveur et navigateur pour empêcher les fuites de données
- Utilisez HydrationBoundary pour transférer les données préchargées du serveur au client
- Organisez les query keys avec des fonctions factory pour une invalidation prévisible
- Implémentez les mises à jour optimistes pour les mutations nécessitant un retour instantané
- Tirez parti des requêtes infinies pour les listes paginées avec chargement automatique
TanStack Query n'est pas qu'une bibliothèque de data fetching — c'est une solution complète de gestion du server state qui réduit le boilerplate, prévient les bugs et rend votre application significativement plus rapide.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Authentifier votre application Next.js 15 avec Auth.js v5 : Email, OAuth et contrôle des rôles
Apprenez à ajouter une authentification prête pour la production à votre application Next.js 15 avec Auth.js v5. Ce guide complet couvre Google OAuth, les identifiants email/mot de passe, les routes protégées, le middleware et le contrôle d'accès basé sur les rôles.

Construire un Chatbot IA Local avec Ollama et Next.js : Guide Complet
Construisez un chatbot IA privé fonctionnant entièrement sur votre machine locale avec Ollama et Next.js. Ce tutoriel pratique couvre l'installation, le streaming des réponses, la sélection de modèles et le déploiement d'une interface de chat prête pour la production — le tout sans envoyer de données dans le cloud.

Construire un site web axé sur le contenu avec Payload CMS 3 et Next.js
Apprenez à construire un site web complet avec Payload CMS 3, qui fonctionne nativement dans Next.js App Router. Ce tutoriel couvre les collections, le rich text, les uploads, l'authentification et le déploiement en production.