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

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

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 bookshelf

Installez TanStack Query v5 et les dépendances associées :

npm install @tanstack/react-query @tanstack/react-query-devtools
npm install zod

La 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 dev

Ouvrez les TanStack Query Devtools (l'icône flottante en bas à droite) pour inspecter :

  1. Requêtes actives — Voir toutes les requêtes en cours et leur état
  2. Entrées du cache — Inspecter les données mises en cache
  3. Timeline des requêtes — Observer les patterns de fetch/refetch
  4. 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 useSuspenseQuery pour un pattern de chargement plus déclaratif
  • Implémentez le support hors-ligne — Utilisez @tanstack/query-persist-client-core pour 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 AbortSignal pour 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 :

  1. Séparez les query clients serveur et navigateur pour empêcher les fuites de données
  2. Utilisez HydrationBoundary pour transférer les données préchargées du serveur au client
  3. Organisez les query keys avec des fonctions factory pour une invalidation prévisible
  4. Implémentez les mises à jour optimistes pour les mutations nécessitant un retour instantané
  5. 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.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Utiliser l'API DeepSeek V3 dans Node.js avec le SDK Vercel AI.

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

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.

25 min read·