TanStack DB avec Next.js : Construire une Base de Données Cliente Réactive en 2026

TanStack DB est la pièce manquante des couches de données React modernes. Si TanStack Query résout brillamment l'état serveur, il laisse un vide quand vous avez besoin d'une réactivité rapide entre composants, de filtrages clients complexes ou de mises à jour optimistes vraiment instantanées. TanStack DB comble ce vide avec un modèle de collections réactives, des requêtes live basées sur le differential dataflow et un support TypeScript de premier ordre — le tout au-dessus du fondement TanStack Query que vous connaissez déjà.
Ce Que Vous Allez Construire
Une application de gestion de projet TaskFlow qui démontre toutes les fonctionnalités essentielles de TanStack DB :
- Collections réactives synchronisées avec un backend REST
- Requêtes live avec jointures, filtres et agrégations
- Mutations optimistes qui semblent instantanées
- Réactivité entre composants sans prop drilling
- Differential dataflow qui ne recalcule que ce qui a changé
- Intégration avec le App Router de Next.js et les Server Components
À la fin, votre interface se mettra à jour en moins d'une milliseconde sur n'importe quel changement de données, même avec des milliers d'éléments.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé
- Une bonne connaissance de React 19 et TypeScript
- Une familiarité avec les bases de TanStack Query
- Une compréhension du App Router de Next.js
- Un éditeur de code (VS Code recommandé)
Pourquoi TanStack DB ?
La gestion d'état React traditionnelle force un compromis. Les bibliothèques d'état serveur comme TanStack Query gèrent le fetching à merveille mais traitent chaque requête comme une entrée de cache isolée. Les bibliothèques d'état client comme Zustand ou Jotai sont réactives mais déconnectées de votre serveur. Les bases de données local-first sont puissantes mais lourdes.
TanStack DB se place entre les deux. C'est un store normalisé et réactif posé au-dessus des collections TanStack Query, avec un moteur de requêtes propulsé par le differential dataflow. Cela signifie que lorsqu'une ligne change, seules les vues qui dépendent de cette ligne sont recalculées — pas tout le jeu de données.
Le résultat ressemble à une petite base de données en mémoire avec des bindings React, tout en conservant le backend TanStack Query familier.
Étape 1 : Configuration du Projet
Créez un nouveau projet Next.js 15 :
npx create-next-app@latest taskflow --typescript --tailwind --eslint --app --src-dir
cd taskflowInstallez TanStack DB et ses dépendances :
npm install @tanstack/react-db @tanstack/db @tanstack/react-query @tanstack/query-core
npm install -D @tanstack/react-query-devtoolsTanStack DB est livré avec des adaptateurs pour plusieurs moteurs de synchronisation. Nous utiliserons l'adaptateur Query collection, qui fonctionne avec n'importe quel backend REST ou GraphQL.
Étape 2 : Définir Votre Schéma
Les collections TanStack DB sont fortement typées. Commencez par définir vos types métier dans src/lib/types.ts :
export type Project = {
id: string;
name: string;
ownerId: string;
createdAt: string;
};
export type Task = {
id: string;
projectId: string;
title: string;
status: "todo" | "doing" | "done";
assigneeId: string | null;
priority: number;
createdAt: string;
};
export type User = {
id: string;
name: string;
avatarUrl: string;
};Ces types circuleront à travers chaque requête, mutation et abonnement, vous offrant une sécurité de type de bout en bout.
Étape 3 : Créer Votre Première Collection
Une collection est l'unité de réactivité dans TanStack DB. Chaque collection est soutenue par une source de synchronisation et expose des requêtes réactives. Créez src/db/collections.ts :
import { createCollection } from "@tanstack/react-db";
import { queryCollectionOptions } from "@tanstack/db-collections";
import type { Project, Task, User } from "@/lib/types";
const API = "/api";
export const projectsCollection = createCollection(
queryCollectionOptions<Project>({
id: "projects",
queryKey: ["projects"],
queryFn: async () => {
const res = await fetch(`${API}/projects`);
if (!res.ok) throw new Error("Échec du chargement des projets");
return res.json();
},
getKey: (project) => project.id,
})
);
export const tasksCollection = createCollection(
queryCollectionOptions<Task>({
id: "tasks",
queryKey: ["tasks"],
queryFn: async () => {
const res = await fetch(`${API}/tasks`);
if (!res.ok) throw new Error("Échec du chargement des tâches");
return res.json();
},
getKey: (task) => task.id,
})
);
export const usersCollection = createCollection(
queryCollectionOptions<User>({
id: "users",
queryKey: ["users"],
queryFn: async () => {
const res = await fetch(`${API}/users`);
if (!res.ok) throw new Error("Échec du chargement des utilisateurs");
return res.json();
},
getKey: (user) => user.id,
})
);Chaque collection déduplique automatiquement les fetchs, met en cache les résultats et émet des événements de changement granulaires lorsque des lignes sont ajoutées, modifiées ou supprimées.
Étape 4 : Brancher le Provider
TanStack DB partage un QueryClient avec TanStack Query. Configurez le provider dans src/app/providers.tsx :
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { useState } from "react";
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
refetchOnWindowFocus: false,
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}Enveloppez l'application dans src/app/layout.tsx :
import { Providers } from "./providers";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="fr">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Étape 5 : Requêtes Live avec useLiveQuery
C'est ici que la magie opère. TanStack DB fournit useLiveQuery, un hook de requête réactive qui ne recalcule que lorsque ses dépendances changent. Créez src/components/TaskList.tsx :
"use client";
import { useLiveQuery, eq } from "@tanstack/react-db";
import { tasksCollection, usersCollection } from "@/db/collections";
type Props = {
projectId: string;
};
export function TaskList({ projectId }: Props) {
const { data: tasks } = useLiveQuery((q) =>
q
.from({ task: tasksCollection })
.join(
{ user: usersCollection },
({ task, user }) => eq(task.assigneeId, user.id),
"left"
)
.where(({ task }) => eq(task.projectId, projectId))
.orderBy(({ task }) => task.priority, "desc")
.select(({ task, user }) => ({
id: task.id,
title: task.title,
status: task.status,
priority: task.priority,
assigneeName: user?.name ?? "Non assigné",
}))
);
return (
<ul className="space-y-2">
{tasks.map((task) => (
<li
key={task.id}
className="rounded-lg border border-slate-200 p-4"
>
<div className="flex items-center justify-between">
<span className="font-medium">{task.title}</span>
<span className="text-sm text-slate-500">
{task.assigneeName}
</span>
</div>
<div className="mt-1 text-xs uppercase text-slate-400">
{task.status} · priorité {task.priority}
</div>
</li>
))}
</ul>
);
}Cette requête joint trois concepts : la collection des tâches, la collection des utilisateurs et un filtre par projet. Comme elle s'exécute via un moteur de differential dataflow, lorsqu'un seul titre de tâche change, seule la ligne contenant cette tâche est recalculée, pas la liste entière.
Étape 6 : Mutations Optimistes
TanStack DB rend les mises à jour optimistes triviales. Créez un helper de mutations dans src/db/mutations.ts :
import { createOptimisticAction } from "@tanstack/react-db";
import { tasksCollection } from "./collections";
import type { Task } from "@/lib/types";
export const updateTaskStatus = createOptimisticAction({
onMutate: ({ taskId, status }: { taskId: string; status: Task["status"] }) => {
tasksCollection.update(taskId, (draft) => {
draft.status = status;
});
},
mutationFn: async ({ taskId, status }) => {
const res = await fetch(`/api/tasks/${taskId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status }),
});
if (!res.ok) throw new Error("Échec de la mise à jour de la tâche");
return res.json();
},
});
export const createTask = createOptimisticAction({
onMutate: (input: Omit<Task, "id" | "createdAt">) => {
const id = crypto.randomUUID();
tasksCollection.insert({
...input,
id,
createdAt: new Date().toISOString(),
});
return { tempId: id };
},
mutationFn: async (input) => {
const res = await fetch("/api/tasks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
});
if (!res.ok) throw new Error("Échec de la création de la tâche");
return res.json();
},
onSuccess: ({ tempId }, serverTask) => {
tasksCollection.replace(tempId, serverTask);
},
});Le bloc onMutate s'exécute de manière synchrone et met à jour la collection locale. Chaque abonné se rerend avant même que la requête réseau commence. Quand le serveur répond, onSuccess réconcilie l'enregistrement temporaire avec celui qui fait autorité.
Si la mutation échoue, TanStack DB annule automatiquement le changement optimiste.
Étape 7 : Construire le Tableau Drag-and-Drop
Combinez requêtes et mutations dans un tableau Kanban dans src/components/TaskBoard.tsx :
"use client";
import { useLiveQuery, eq } from "@tanstack/react-db";
import { tasksCollection } from "@/db/collections";
import { updateTaskStatus } from "@/db/mutations";
import type { Task } from "@/lib/types";
const COLUMNS: Task["status"][] = ["todo", "doing", "done"];
export function TaskBoard({ projectId }: { projectId: string }) {
const { data: tasksByStatus } = useLiveQuery((q) =>
q
.from({ task: tasksCollection })
.where(({ task }) => eq(task.projectId, projectId))
.groupBy(({ task }) => task.status)
.select(({ task }) => ({
status: task.status,
items: task,
}))
);
const handleDrop = (taskId: string, status: Task["status"]) => {
updateTaskStatus.mutate({ taskId, status });
};
return (
<div className="grid grid-cols-3 gap-4">
{COLUMNS.map((status) => {
const column = tasksByStatus.find((c) => c.status === status);
return (
<Column
key={status}
status={status}
tasks={column?.items ?? []}
onDrop={handleDrop}
/>
);
})}
</div>
);
}
function Column({
status,
tasks,
onDrop,
}: {
status: Task["status"];
tasks: Task[];
onDrop: (taskId: string, status: Task["status"]) => void;
}) {
return (
<div
className="rounded-lg bg-slate-50 p-3"
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
const taskId = e.dataTransfer.getData("text/plain");
onDrop(taskId, status);
}}
>
<h3 className="mb-3 font-semibold uppercase">{status}</h3>
{tasks.map((task) => (
<div
key={task.id}
draggable
onDragStart={(e) =>
e.dataTransfer.setData("text/plain", task.id)
}
className="mb-2 cursor-grab rounded-md bg-white p-3 shadow-sm"
>
{task.title}
</div>
))}
</div>
);
}Faites glisser une carte de "todo" vers "doing" et tout le tableau se met à jour instantanément. La mutation optimiste réécrit la ligne locale, le moteur différentiel relance le bucket groupBy concerné, et React applique un patch précis.
Étape 8 : Réactivité Entre Composants
Une seule requête live s'abonne une fois mais peut alimenter un nombre quelconque de composants. Créez une barre latérale qui affiche les compteurs agrégés dans src/components/SidebarStats.tsx :
"use client";
import { useLiveQuery, count, eq } from "@tanstack/react-db";
import { tasksCollection } from "@/db/collections";
export function SidebarStats({ projectId }: { projectId: string }) {
const { data: stats } = useLiveQuery((q) =>
q
.from({ task: tasksCollection })
.where(({ task }) => eq(task.projectId, projectId))
.groupBy(({ task }) => task.status)
.select(({ task }) => ({
status: task.status,
total: count(task.id),
}))
);
return (
<aside className="space-y-3 p-4">
{stats.map((row) => (
<div key={row.status} className="flex justify-between">
<span className="capitalize">{row.status}</span>
<span className="font-semibold">{row.total}</span>
</div>
))}
</aside>
);
}Le tableau et la barre latérale s'abonnent tous deux à la même collection de tâches sous-jacente. Lorsque vous déplacez une tâche entre les colonnes, les deux composants se mettent à jour à partir du même changement incrémental. Aucune invalidation de cache manuelle, aucun prop drilling, aucun bus d'événements.
Étape 9 : Intégration avec les Server Components
Le App Router de Next.js permet de précharger côté serveur. Les collections TanStack DB peuvent être hydratées depuis des données rendues côté serveur. Créez un loader serveur dans src/app/projects/[id]/page.tsx :
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import { getQueryClient } from "@/lib/get-query-client";
import { TaskBoard } from "@/components/TaskBoard";
import { SidebarStats } from "@/components/SidebarStats";
export default async function ProjectPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const queryClient = getQueryClient();
await Promise.all([
queryClient.prefetchQuery({
queryKey: ["tasks"],
queryFn: () => fetch(`${process.env.API_URL}/tasks`).then((r) => r.json()),
}),
queryClient.prefetchQuery({
queryKey: ["users"],
queryFn: () => fetch(`${process.env.API_URL}/users`).then((r) => r.json()),
}),
]);
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<div className="grid grid-cols-[260px_1fr] gap-6 p-6">
<SidebarStats projectId={id} />
<TaskBoard projectId={id} />
</div>
</HydrationBoundary>
);
}Comme TanStack DB s'appuie sur TanStack Query, la frontière d'hydratation standard fonctionne sans modification. Votre collection arrive remplie au premier rendu sans aucun scintillement de chargement.
Étape 10 : Index Personnalisés pour la Vitesse
Pour les collections avec des milliers de lignes, vous pouvez ajouter des index pour accélérer les filtres. Mettez à jour la définition de la collection des tâches :
export const tasksCollection = createCollection(
queryCollectionOptions<Task>({
id: "tasks",
queryKey: ["tasks"],
queryFn: loadTasks,
getKey: (task) => task.id,
indexes: [
{ id: "byProject", keyFn: (task) => task.projectId },
{ id: "byStatus", keyFn: (task) => task.status },
{ id: "byAssignee", keyFn: (task) => task.assigneeId ?? "" },
],
})
);Le planificateur de requêtes utilise ces index automatiquement quand votre clause where correspond. Un filtre par projectId sur dix mille tâches s'exécute désormais en microsecondes plutôt qu'en scan complet.
Étape 11 : Synchronisation Temps Réel avec Server-Sent Events
Associez TanStack DB avec SSE pour une collaboration multi-utilisateurs en direct. Créez src/hooks/useTaskSync.ts :
"use client";
import { useEffect } from "react";
import { tasksCollection } from "@/db/collections";
import type { Task } from "@/lib/types";
type Event =
| { type: "task.created"; task: Task }
| { type: "task.updated"; task: Task }
| { type: "task.deleted"; taskId: string };
export function useTaskSync(projectId: string) {
useEffect(() => {
const source = new EventSource(`/api/projects/${projectId}/stream`);
source.onmessage = (event) => {
const payload: Event = JSON.parse(event.data);
switch (payload.type) {
case "task.created":
tasksCollection.upsert(payload.task);
break;
case "task.updated":
tasksCollection.upsert(payload.task);
break;
case "task.deleted":
tasksCollection.delete(payload.taskId);
break;
}
};
return () => source.close();
}, [projectId]);
}Montez ce hook une fois par page. Chaque client connecté voit alors les mêmes mises à jour de tâches en temps réel, avec la même efficacité différentielle que les mutations locales.
Tester Votre Implémentation
Lancez le serveur de développement et ouvrez deux fenêtres de navigateur côte à côte :
npm run devFaites glisser une tâche dans la fenêtre une. La carte saute de colonne instantanément dans la fenêtre une et arrive dans la fenêtre deux dans l'aller-retour SSE. Ouvrez le profileur React DevTools et confirmez que seuls les composants concernés se re-rendent. Ajoutez cent tâches supplémentaires et constatez l'absence de tout ralentissement perceptible.
Dépannage
La requête live retourne des données obsolètes après navigation. Assurez-vous que votre provider enveloppe toute la mise en page, pas une seule page. Un nouveau QueryClient invalide le cache de la collection.
La mise à jour optimiste scintille quand le serveur répond. Cela signifie généralement que le serveur retourne un enregistrement avec une forme différente de l'insertion optimiste. Alignez les deux formes ou utilisez onSuccess pour mapper la charge utile serveur avant la réconciliation.
L'index ne semble pas être utilisé. Les index ne fonctionnent que pour les filtres d'égalité. Un filtre de plage ou un appel de fonction sur le champ indexé retombe sur un scan complet.
TypeScript se plaint des assignees null dans la jointure. Utilisez une jointure gauche et marquez le côté utilisateur comme optionnel dans le callback select. Le schéma fait savoir au système de types que l'utilisateur peut être manquant.
Étapes Suivantes
- Essayez l'adaptateur Electric SQL pour une synchronisation bidirectionnelle complète sans écrire de code SSE
- Explorez Local-First avec Yjs et React pour les patterns offline-first
- Combinez TanStack DB avec Drizzle ORM côté serveur pour une sécurité de type de bout en bout
- Ajoutez les fonctions durables Inngest pour le traitement en arrière-plan des mutations
Conclusion
TanStack DB complète l'image que TanStack Query a commencée. En posant un modèle de collections réactives et un moteur de requêtes differential dataflow par-dessus le cache auquel vous faites déjà confiance, il élimine la friction entre les données serveur et les vues qui en dépendent. Votre application cesse de raisonner sur les états de chargement et l'invalidation du cache et commence à raisonner sur les données — propres, normalisées et réactives de bout en bout.
La bibliothèque est encore jeune, mais sa surface d'API est déjà focalisée et ergonomique. Si vous avez déjà construit un tableau de bord ou un outil collaboratif en React et vous êtes retrouvé à lutter contre des états obsolètes, donnez un projet à TanStack DB et regardez les rugosités disparaître.
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

Jotai 2 — Gestion d'état atomique pour React et Next.js : de zéro à la production
Maîtrisez la gestion d'état atomique dans React avec Jotai 2. Ce tutoriel pratique couvre les atomes primitifs, dérivés et asynchrones, le stockage persistant, l'hydratation SSR avec le Next.js App Router, et les patterns concrets pour construire des applications scalables.

Construire des applications collaboratives Local-First avec Yjs et React
Apprenez à construire des applications collaboratives en temps réel fonctionnant hors ligne avec les CRDTs Yjs et React. Ce tutoriel couvre la synchronisation sans conflits, l'architecture offline-first et la création d'un éditeur de documents partagé.

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.