Tout frontend un peu sérieux finit par se noyer dans le même problème : des données récupérées depuis une dizaine d'endpoints, dispersées entre l'état React, le contexte et un cache, le tout se rendant bien plus souvent qu'il ne le devrait. On mémoïse, on remonte l'état, on ajoute un store global, et l'application reste poussive dès qu'une liste dépasse quelques milliers de lignes.
TanStack DB est la réponse de l'équipe à ce désordre. C'est un store réactif côté client qui se place au-dessus de TanStack Query, sans la remplacer. Query continue de faire ce qu'elle fait le mieux — récupérer, mettre en cache et revalider depuis le serveur — et alimente ces données dans une base locale interrogeable, dotée de vraies relations, de requêtes en direct et d'écritures optimistes. Le chiffre marquant : modifier une ligne dans une collection triée de 100 000 éléments s'effectue en environ 0,7 ms sur un M1 Pro. Assez rapide pour que l'interface optimiste cesse d'être une astuce et devienne le comportement par défaut.
Ce guide parcourt les concepts fondamentaux et l'API que vous utiliserez réellement.
Les trois primitives : collections, requêtes en direct, transactions
TanStack DB repose sur trois idées qui se composent proprement :
- Les collections sont des ensembles typés d'objets — vos
todos,issuesouusers. Chaque élément possède une clé stable. Une collection est alimentée par une source : un endpoint TanStack Query, une shape ElectricSQL, le stockage local, ou une fonction de synchronisation entièrement personnalisée. - Les requêtes en direct lisent une ou plusieurs collections via un constructeur de requêtes relationnel. Elles sont réactives : quand les données sous-jacentes changent d'une manière qui affecte le résultat, seule la partie concernée est recalculée et réaffichée.
- Les transactions encapsulent les mutations. Vous modifiez les données de façon optimiste dans la collection locale, l'interface se met à jour immédiatement, et un gestionnaire persiste le changement vers votre backend en arrière-plan — avec rollback automatique en cas d'échec.
Ce qui fait que tout cela dépasse un simple cache élégant, c'est le moteur sous les requêtes en direct.
Flux différentiel : pourquoi les requêtes passent sous la milliseconde
La plupart des bibliothèques d'état côté client relancent un sélecteur ou un filtre depuis zéro dès que quoi que ce soit change. TanStack DB utilise plutôt le flux différentiel (differential dataflow), implémenté en TypeScript via une bibliothèque nommée d2ts. Au lieu de recalculer une jointure, un filtre ou une agrégation sur l'ensemble du jeu de données, elle ne recalcule que le delta — les lignes qui ont réellement changé.
L'effet pratique : une requête complexe joignant deux collections et triant 100 000 lignes se met à jour de façon incrémentale en moins d'une milliseconde lorsqu'une seule ligne change. Vous pouvez exprimer la logique relationnelle dont les applications produit ont finalement besoin — jointures inter-collections, agrégations, tri — sans payer la taxe de re-rendu habituelle. C'est cette différence qui rend instantanés les grands graphes de données locaux.
Mettre en place une collection
Une collection adossée à TanStack Query a besoin d'une clé de requête, d'une fonction de récupération, d'un extracteur de clé et de gestionnaires de mutation optionnels. Voici une collection todos reliée à une API REST :
import { createCollection } from '@tanstack/react-db'
import { queryCollectionOptions } from '@tanstack/query-db-collection'
const todoCollection = createCollection(
queryCollectionOptions({
queryKey: ['todos'],
queryFn: async () => {
const response = await fetch('/api/todos')
return response.json()
},
getKey: (item) => item.id,
onInsert: async ({ transaction }) => {
const { modified: newTodo } = transaction.mutations[0]
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
})
},
onUpdate: async ({ transaction }) => {
const { original, modified } = transaction.mutations[0]
await fetch(`/api/todos/${original.id}`, {
method: 'PUT',
body: JSON.stringify(modified),
})
},
onDelete: async ({ transaction }) => {
const { original } = transaction.mutations[0]
await fetch(`/api/todos/${original.id}`, { method: 'DELETE' })
},
})
)Notez que onInsert, onUpdate et onDelete reçoivent une transaction contenant une ou plusieurs mutations. La collection applique d'abord le changement localement, puis votre gestionnaire dialogue avec le serveur. Pour une collection adossée à une requête, le gestionnaire ne doit pas se résoudre tant que l'état serveur n'est pas resynchronisé — TanStack DB relance la récupération automatiquement une fois le gestionnaire terminé, réconciliant l'état optimiste avec la vérité.
Lire les données avec useLiveQuery
On interroge les collections via un constructeur plutôt que des méthodes de tableau brutes. Le constructeur reflète la sémantique SQL — from, where, join, select, orderBy — tout en restant entièrement typé de bout en bout. La requête en direct la plus simple filtre une seule collection :
import { useLiveQuery, eq } from '@tanstack/react-db'
function ActiveTodos() {
const { data, isLoading } = useLiveQuery((q) =>
q.from({ todos: todoCollection })
.where(({ todos }) => eq(todos.completed, false))
.select(({ todos }) => ({ id: todos.id, text: todos.text }))
)
if (isLoading) return <p>Chargement…</p>
return <ul>{data.map((t) => <li key={t.id}>{t.text}</li>)}</ul>
}Le composant ne se rend à nouveau que lorsqu'un todo correspondant au filtre est ajouté, supprimé ou modifié — pas à chaque écriture dans le cache. Les jointures se lisent aussi proprement qu'une instruction SQL, résolvant les relations entre collections sans normalisation manuelle :
const { data } = useLiveQuery((q) =>
q.from({ issues: issueCollection })
.join({ persons: personCollection }, ({ issues, persons }) =>
eq(issues.userId, persons.id)
)
.select(({ issues, persons }) => ({
id: issues.id,
title: issues.title,
userName: persons.name,
}))
)Les requêtes en direct se réexécutent aussi lorsqu'une dépendance réactive change. Passez la dépendance et le constructeur lit sa valeur courante — par exemple un seuil de priorité contrôlé par un état de composant :
const { data } = useLiveQuery(
(q) => q.from({ todos: todoCollection })
.where(({ todos }) => gt(todos.priority, minPriority))
)Des mutations optimistes vraiment instantanées
Parce que la collection locale est la source de vérité de l'interface, les écritures sont immédiates. Appeler insert, update ou delete sur une collection renvoie une transaction dont vous pouvez attendre la persistance :
// Insertion — l'UI se met à jour instantanément, la synchro serveur se fait dans le gestionnaire
const tx = todoCollection.insert({
id: crypto.randomUUID(),
text: 'Livrer la fonctionnalité',
completed: false,
})
await tx.isPersisted.promise // se résout une fois le backend confirméLe flux est : appliquer localement, afficher, persister, réconcilier. Si la requête backend échoue, la transaction annule automatiquement le changement optimiste, de sorte qu'un appel réseau raté ne laisse jamais de ligne fantôme à l'écran. C'est pourquoi l'interface optimiste dans TanStack DB ne demande aucune chirurgie manuelle du cache — le rollback et la resynchro font partie du modèle.
Choisir un moteur de synchronisation
L'abstraction de collection est volontairement agnostique du backend. Vous commencez avec ce que vous avez déjà et changez de source plus tard, sans réécrire vos composants ni vos requêtes :
- queryCollectionOptions — n'importe quel endpoint REST, GraphQL ou tRPC, via TanStack Query.
- electricCollectionOptions — synchro temps réel depuis Postgres via les shapes ElectricSQL.
- localOnlyCollectionOptions — état purement côté client, sans backend.
- localStorageCollectionOptions — persisté dans le stockage local du navigateur, d'un rechargement à l'autre.
Comme le constructeur de requêtes et l'API useLiveQuery sont identiques pour tous, vous pouvez prototyper contre un simple endpoint REST puis passer à un moteur de synchro en flux quand vous avez besoin d'une vraie collaboration temps réel — sans toucher au côté lecture de votre application.
Où elle s'inscrit, et une note pour les équipes MENA
TanStack DB est en bêta au moment d'écrire ces lignes : traitez-la comme prometteuse pour la production plutôt que durcie pour la production, et figez vos versions. Elle brille pour les surfaces produit denses en données et interactives — tableaux de bord, suivis de tickets, boîtes de réception, panneaux d'administration — où vous avez beaucoup d'entités, des mises à jour fréquentes et un besoin de jointures côté client. Pour un site vitrine majoritairement statique, c'est surdimensionné ; TanStack Query seule suffit.
Pour les équipes qui construisent pour les marchés MENA, l'angle local-first mérite réflexion. Une collection localOnly ou localStorage garde l'état de travail sur l'appareil, ce qui peut réduire les allers-retours sur des connexions irrégulières et conserver davantage de données utilisateur côté client — un levier utile lorsque l'on raisonne sur les exigences de traitement des données de l'INPDP tunisien ou du PDPL saoudien. Quand vous adoptez ensuite un moteur de synchro comme ElectricSQL, la question de l'emplacement de Postgres devient une décision de résidence que vous maîtrisez, et non un défaut hérité.
À retenir
TanStack DB recadre l'état côté client comme une petite base relationnelle rapide plutôt qu'un amas de hooks et de sélecteurs mémoïsés. Les collections normalisent vos données, le flux différentiel rend les requêtes en direct quasi gratuites, et les transactions font des écritures optimistes le chemin de moindre résistance. Si le frontend de votre application a dépassé le cache ad hoc, c'est la réponse la plus cohérente que l'écosystème TanStack ait livrée — et elle s'insère aux côtés des appels useQuery que vous avez déjà.