QueryCollectionElectricCollectionTrailBaseCollectionLocalStorageCollectionLocalOnlyCollectionuseLiveQuery HookqueryBuildermutationFncreateOptimisticActioninsertupdatedeleteoptimistic: false verwendetWillkommen in der TanStack DB Dokumentation.
TanStack DB ist ein reaktiver Client-Store zum Erstellen blitzschneller Apps mit Sync. Es erweitert TanStack Query um Collections, Live Queries und optimistische Mutationen.
TanStack DB funktioniert durch
// Define collections to load data into
const todoCollection = createCollection({
// ...your config
onUpdate: updateMutationFn,
})
const Todos = () => {
// Bind data using live queries
const { data: todos } = useLiveQuery((q) =>
q.from({ todo: todoCollection }).where(({ todo }) => todo.completed)
)
const complete = (todo) => {
// Instantly applies optimistic state
todoCollection.update(todo.id, (draft) => {
draft.completed = true
})
}
return (
<ul>
{todos.map((todo) => (
<li key={todo.id} onClick={() => complete(todo)}>
{todo.text}
</li>
))}
</ul>
)
}
// Define collections to load data into
const todoCollection = createCollection({
// ...your config
onUpdate: updateMutationFn,
})
const Todos = () => {
// Bind data using live queries
const { data: todos } = useLiveQuery((q) =>
q.from({ todo: todoCollection }).where(({ todo }) => todo.completed)
)
const complete = (todo) => {
// Instantly applies optimistic state
todoCollection.update(todo.id, (draft) => {
draft.completed = true
})
}
return (
<ul>
{todos.map((todo) => (
<li key={todo.id} onClick={() => complete(todo)}>
{todo.text}
</li>
))}
</ul>
)
}
Collections sind typisierte Mengen von Objekten, die mit Daten gefüllt werden können. Sie sind darauf ausgelegt, das Laden von Daten in Ihre App vom Binden von Daten an Ihre Komponenten zu entkoppeln.
Collections können auf viele Arten befüllt werden, einschließlich
Sobald Sie Ihre Daten in Collections haben, können Sie diese mithilfe von Live Queries in Ihren Komponenten abfragen.
Live Queries werden verwendet, um Daten aus Collections abzufragen. Live Queries sind reaktiv: Wenn sich die zugrundeliegenden Daten so ändern, dass das Abfrageergebnis beeinflusst wird, wird das Ergebnis inkrementell aktualisiert und aus der Abfrage zurückgegeben, was eine Neu-Renderung auslöst.
TanStack DB Live Queries werden mit d2ts implementiert, einer TypeScript-Implementierung von Differential Dataflow. Dies ermöglicht die *inkrementelle* Aktualisierung der Abfrageergebnisse (anstatt die gesamte Abfrage neu auszuführen). Das macht sie extrem schnell, oft unter einer Millisekunde, selbst bei sehr komplexen Abfragen.
Live Queries unterstützen Joins zwischen Collections. Dies ermöglicht Ihnen,
Jede Abfrage gibt eine weitere Collection zurück, die *auch* abgefragt werden kann.
Weitere Details zu Live Queries finden Sie in der Dokumentation zu Live Queries.
Collections unterstützen insert-, update- und delete-Operationen. Wenn diese aufgerufen werden, lösen sie standardmäßig die entsprechenden onInsert-, onUpdate- und onDelete-Handler aus, die für das Schreiben der Mutation an das Backend verantwortlich sind.
// Define collection with persistence handlers
const todoCollection = createCollection({
id: "todos",
// ... other config
onUpdate: async ({ transaction }) => {
const { original, changes } = transaction.mutations[0]
await api.todos.update(original.id, changes)
},
})
// Immediately applies optimistic state
todoCollection.update(todo.id, (draft) => {
draft.completed = true
})
// Define collection with persistence handlers
const todoCollection = createCollection({
id: "todos",
// ... other config
onUpdate: async ({ transaction }) => {
const { original, changes } = transaction.mutations[0]
await api.todos.update(original.id, changes)
},
})
// Immediately applies optimistic state
todoCollection.update(todo.id, (draft) => {
draft.completed = true
})
Anstatt die Collection-Daten direkt zu mutieren, behandelt die Collection intern ihre synchronisierten/geladenen Daten als unveränderlich und verwaltet eine separate Menge lokaler Mutationen als optimistischen Zustand. Wenn Live Queries aus der Collection lesen, sehen sie eine lokale Ansicht, die die lokalen optimistischen Mutationen über die unveränderlichen synchronisierten Daten legt.
Der optimistische Zustand wird gehalten, bis der onUpdate-Handler (in diesem Fall) erfolgreich ist – zu diesem Zeitpunkt werden die Daten an den Server übertragen und zurück an die lokale Collection synchronisiert.
Wenn der Handler einen Fehler auslöst, wird der optimistische Zustand zurückgerollt.
Mutationen basieren auf einem Transaction-Primitive.
Für einfache Zustandsänderungen reicht die direkte Mutation der Collection und die Persistierung mit den Operator-Handlern aus.
Für komplexere Anwendungsfälle können Sie jedoch direkt benutzerdefinierte Aktionen mit createOptimisticAction oder benutzerdefinierte Transaktionen mit createTransaction erstellen. Dies ermöglicht Ihnen, Dinge wie Transaktionen mit mehreren Mutationen über mehrere Collections hinweg, verkettete Transaktionen mit zwischenzeitlichen Rollbacks usw. durchzuführen.
Im folgenden Code beispielsweise sendet die mutationFn zunächst den Schreibvorgang an den Server mit await api.todos.update(updatedTodo) und ruft dann await collection.refetch() auf, um ein erneutes Laden der Collection-Inhalte über TanStack Query auszulösen. Wenn das zweite await erfolgreich ist, ist die Collection auf dem neuesten Stand mit den letzten Änderungen und der optimistische Zustand wird sicher verworfen.
const updateTodo = createOptimisticAction<{ id: string }>({
onMutate,
mutationFn: async ({ transaction }) => {
const { collection, modified: updatedTodo } = transaction.mutations[0]
await api.todos.update(updatedTodo)
await collection.refetch()
},
})
const updateTodo = createOptimisticAction<{ id: string }>({
onMutate,
mutationFn: async ({ transaction }) => {
const { collection, modified: updatedTodo } = transaction.mutations[0]
await api.todos.update(updatedTodo)
await collection.refetch()
},
})
Dies kombiniert sich, um ein Modell des uni-direktionalen Datenflusses zu unterstützen, das das Muster des State Managements im Redux/Flux-Stil über den Client hinaus erweitert, um auch den Server einzubeziehen.
Mit einer sofortigen inneren Schleife von optimistischem Zustand, der zeitlich von der langsameren äußeren Schleife des Persistierens auf dem Server und des Zurücksynchronisierens des aktualisierten Serverzustands in die Collection abgelöst wird.
Es gibt eine Reihe von integrierten Collection-Typen
Sie können auch verwenden
Alle Collections unterstützen optional (aber dringend empfohlen) die Hinzufügung eines Schemas.
Wenn angegeben, muss dies eine mit Standard Schema kompatible Schema-Instanz sein, wie z.B. ein Zod- oder Effect-Schema.
Die Collection verwendet das Schema zur clientseitigen Validierung optimistischer Mutationen.
Die Collection verwendet das Schema für ihren Typ. Wenn Sie also ein Schema angeben, können Sie keinen expliziten Typ übergeben (z.B. createCollection<Todo>()).
TanStack Query ruft Daten über verwaltete Abfragen ab. Verwenden Sie queryCollectionOptions, um Daten mithilfe von TanStack Query in eine Collection zu laden.
import { createCollection } from "@tanstack/react-db"
import { queryCollectionOptions } from "@tanstack/query-db-collection"
const todoCollection = createCollection(
queryCollectionOptions({
queryKey: ["todoItems"],
queryFn: async () => {
const response = await fetch("/api/todos");
return response.json();
},
getKey: (item) => item.id,
schema: todoSchema, // any standard schema
})
)
import { createCollection } from "@tanstack/react-db"
import { queryCollectionOptions } from "@tanstack/query-db-collection"
const todoCollection = createCollection(
queryCollectionOptions({
queryKey: ["todoItems"],
queryFn: async () => {
const response = await fetch("/api/todos");
return response.json();
},
getKey: (item) => item.id,
schema: todoSchema, // any standard schema
})
)
Die Collection wird mit den Abfrageergebnissen befüllt.
Electric ist eine Read-Path-Sync-Engine für Postgres. Sie ermöglicht es Ihnen, Teilmengen von Daten aus einer Postgres-Datenbank über Ihre API in eine TanStack DB Collection zu synchronisieren.
Das Haupt-Primitive von Electric für Sync ist ein Shape. Verwenden Sie electricCollectionOptions, um einen Shape in eine Collection zu synchronisieren.
import { createCollection } from "@tanstack/react-db"
import { electricCollectionOptions } from "@tanstack/electric-db-collection"
export const todoCollection = createCollection(
electricCollectionOptions({
id: "todos",
shapeOptions: {
url: "https://example.com/v1/shape",
params: {
table: "todos",
},
},
getKey: (item) => item.id,
schema: todoSchema,
})
)
import { createCollection } from "@tanstack/react-db"
import { electricCollectionOptions } from "@tanstack/electric-db-collection"
export const todoCollection = createCollection(
electricCollectionOptions({
id: "todos",
shapeOptions: {
url: "https://example.com/v1/shape",
params: {
table: "todos",
},
},
getKey: (item) => item.id,
schema: todoSchema,
})
)
Die Electric Collection benötigt zwei Electric-spezifische Optionen
Eine neue Collection beginnt erst mit der Synchronisation, wenn Sie collection.preload() aufrufen oder sie abfragen.
Electric Shapes ermöglichen es Ihnen, Daten mithilfe von WHERE-Klauseln zu filtern.
export const myPendingTodos = createCollection(
electricCollectionOptions({
id: "todos",
shapeOptions: {
url: "https://example.com/v1/shape",
params: {
table: "todos",
where: `
status = 'pending'
AND
user_id = '${user.id}'
`,
},
},
getKey: (item) => item.id,
schema: todoSchema,
})
)
export const myPendingTodos = createCollection(
electricCollectionOptions({
id: "todos",
shapeOptions: {
url: "https://example.com/v1/shape",
params: {
table: "todos",
where: `
status = 'pending'
AND
user_id = '${user.id}'
`,
},
},
getKey: (item) => item.id,
schema: todoSchema,
})
)
Tipp
Shape WHERE-Klauseln, die zum Filtern von Daten verwendet werden, die in ElectricCollections synchronisiert werden, unterscheiden sich von den Live Queries, die Sie zum Abfragen von Daten in Komponenten verwenden.
Live Queries sind wesentlich ausdrucksstärker als Shapes und ermöglichen es Ihnen, Collections zu verbinden, zu aggregieren usw. Shapes enthalten lediglich gefilterte Datenbanktabellen und dienen zum Befüllen der Daten in einer Collection.
Wenn Sie mehr Kontrolle darüber benötigen, welche Daten in die Collection synchronisiert werden, ermöglicht Electric es Ihnen, Ihre API als Proxy zu verwenden, um sowohl Daten zu autorisieren als auch zu filtern.
Siehe die Electric Docs für weitere Informationen.
TrailBase ist ein einfach selbst zu hostendes, einzelnes ausführbares Anwendungsbackend mit integriertem SQLite, einer V8 JS-Laufzeit, Authentifizierung, Admin-UIs und Synchronisationsfunktionen.
TrailBase ermöglicht es Ihnen, Tabellen über Record APIs bereitzustellen und Änderungen zu abonnieren, wenn enable_subscriptions gesetzt ist. Verwenden Sie trailBaseCollectionOptions, um Records in eine Collection zu synchronisieren.
import { createCollection } from "@tanstack/react-db"
import { trailBaseCollectionOptions } from "@tanstack/trailbase-db-collection"
import { initClient } from "trailbase"
const trailBaseClient = initClient(`https://trailbase.io`)
export const todoCollection = createCollection<SelectTodo, Todo>(
electricCollectionOptions({
id: "todos",
recordApi: trailBaseClient.records(`todos`),
getKey: (item) => item.id,
schema: todoSchema,
parse: {
created_at: (ts) => new Date(ts * 1000),
},
serialize: {
created_at: (date) => Math.floor(date.valueOf() / 1000),
},
})
)
import { createCollection } from "@tanstack/react-db"
import { trailBaseCollectionOptions } from "@tanstack/trailbase-db-collection"
import { initClient } from "trailbase"
const trailBaseClient = initClient(`https://trailbase.io`)
export const todoCollection = createCollection<SelectTodo, Todo>(
electricCollectionOptions({
id: "todos",
recordApi: trailBaseClient.records(`todos`),
getKey: (item) => item.id,
schema: todoSchema,
parse: {
created_at: (ts) => new Date(ts * 1000),
},
serialize: {
created_at: (date) => Math.floor(date.valueOf() / 1000),
},
})
)
Diese Collection erfordert die folgenden TrailBase-spezifischen Optionen
Eine neue Collection beginnt erst mit der Synchronisation, wenn Sie collection.preload() aufrufen oder sie abfragen.
LocalStorage Collections speichern kleine Mengen lokaler Daten, die über Browser-Sitzungen hinweg persistent sind und über Browser-Tabs in Echtzeit synchronisiert werden. Alle Daten werden unter einem einzigen localStorage-Schlüssel gespeichert und automatisch über Storage-Events synchronisiert.
Verwenden Sie localStorageCollectionOptions, um eine Collection zu erstellen, die Daten in localStorage speichert.
import { createCollection } from "@tanstack/react-db"
import { localStorageCollectionOptions } from "@tanstack/react-db"
export const userPreferencesCollection = createCollection(
localStorageCollectionOptions({
id: "user-preferences",
storageKey: "app-user-prefs", // localStorage key
getKey: (item) => item.id,
schema: userPrefsSchema,
})
)
import { createCollection } from "@tanstack/react-db"
import { localStorageCollectionOptions } from "@tanstack/react-db"
export const userPreferencesCollection = createCollection(
localStorageCollectionOptions({
id: "user-preferences",
storageKey: "app-user-prefs", // localStorage key
getKey: (item) => item.id,
schema: userPrefsSchema,
})
)
Die LocalStorage Collection erfordert
Mutationshandler (onInsert, onUpdate, onDelete) sind völlig optional. Daten werden in localStorage gespeichert, unabhängig davon, ob Sie Handler bereitstellen oder nicht. Sie können alternative Speicher-Backends wie sessionStorage oder benutzerdefinierte Implementierungen bereitstellen, die die localStorage-API nachahmen.
export const sessionCollection = createCollection(
localStorageCollectionOptions({
id: "session-data",
storageKey: "session-key",
storage: sessionStorage, // Use sessionStorage instead
getKey: (item) => item.id,
})
)
export const sessionCollection = createCollection(
localStorageCollectionOptions({
id: "session-data",
storageKey: "session-key",
storage: sessionStorage, // Use sessionStorage instead
getKey: (item) => item.id,
})
)
Tipp
LocalStorage Collections sind ideal für Benutzereinstellungen, UI-Zustände und andere Daten, die lokal persistent sein sollen, aber keine Server-Synchronisation benötigen. Für Server-synchronisierte Daten verwenden Sie stattdessen QueryCollection oder ElectricCollection.
LocalOnly Collections sind für In-Memory-Client-Daten oder UI-Zustände konzipiert, die nicht über Browser-Sitzungen hinweg persistent sein oder über Tabs synchronisiert werden müssen. Sie bieten eine einfache Möglichkeit, temporäre, nur für die Sitzung gültige Daten mit voller Unterstützung für optimistische Mutationen zu verwalten.
Verwenden Sie localOnlyCollectionOptions, um eine Collection zu erstellen, die Daten nur im Speicher speichert.
import { createCollection } from "@tanstack/react-db"
import { localOnlyCollectionOptions } from "@tanstack/react-db"
export const uiStateCollection = createCollection(
localOnlyCollectionOptions({
id: "ui-state",
getKey: (item) => item.id,
schema: uiStateSchema,
// Optional initial data to populate the collection
initialData: [
{ id: "sidebar", isOpen: false },
{ id: "theme", mode: "light" },
],
})
)
import { createCollection } from "@tanstack/react-db"
import { localOnlyCollectionOptions } from "@tanstack/react-db"
export const uiStateCollection = createCollection(
localOnlyCollectionOptions({
id: "ui-state",
getKey: (item) => item.id,
schema: uiStateSchema,
// Optional initial data to populate the collection
initialData: [
{ id: "sidebar", isOpen: false },
{ id: "theme", mode: "light" },
],
})
)
Die LocalOnly Collection erfordert
Optionale Konfiguration
Mutationshandler sind völlig optional. Wenn sie bereitgestellt werden, werden sie aufgerufen, bevor der optimistische Zustand bestätigt wird. Die Collection verwaltet intern automatisch den Übergang vom optimistischen zum bestätigten Zustand.
export const tempDataCollection = createCollection(
localOnlyCollectionOptions({
id: "temp-data",
getKey: (item) => item.id,
onInsert: async ({ transaction }) => {
// Custom logic before confirming the insert
console.log("Inserting:", transaction.mutations[0].modified)
},
onUpdate: async ({ transaction }) => {
// Custom logic before confirming the update
const { original, modified } = transaction.mutations[0]
console.log("Updating from", original, "to", modified)
},
})
)
export const tempDataCollection = createCollection(
localOnlyCollectionOptions({
id: "temp-data",
getKey: (item) => item.id,
onInsert: async ({ transaction }) => {
// Custom logic before confirming the insert
console.log("Inserting:", transaction.mutations[0].modified)
},
onUpdate: async ({ transaction }) => {
// Custom logic before confirming the update
const { original, modified } = transaction.mutations[0]
console.log("Updating from", original, "to", modified)
},
})
)
Tipp
LocalOnly Collections sind ideal für temporäre UI-Zustände, Formulardaten oder beliebige Client-seitige Daten, die keine Persistenz benötigen. Für Daten, die über Sitzungen hinweg persistent sein sollen, verwenden Sie stattdessen LocalStorageCollection.
Live Queries geben Collections zurück. Dies ermöglicht es Ihnen, Collections von anderen Collections abzuleiten.
Zum Beispiel
import { createLiveQueryCollection, eq } from "@tanstack/db"
// Imagine you have a collection of todos.
const todoCollection = createCollection({
// config
})
// You can derive a new collection that's a subset of it.
const completedTodoCollection = createLiveQueryCollection({
startSync: true,
query: (q) =>
q.from({ todo: todoCollection }).where(({ todo }) => todo.completed),
})
import { createLiveQueryCollection, eq } from "@tanstack/db"
// Imagine you have a collection of todos.
const todoCollection = createCollection({
// config
})
// You can derive a new collection that's a subset of it.
const completedTodoCollection = createLiveQueryCollection({
startSync: true,
query: (q) =>
q.from({ todo: todoCollection }).where(({ todo }) => todo.completed),
})
Dies funktioniert auch mit Joins, um Collections aus mehreren Quell-Collections abzuleiten. Und es funktioniert rekursiv – Sie können Collections von anderen abgeleiteten Collections ableiten. Änderungen propagieren effizient mittels Differential Dataflow und es sind Collections bis ganz nach unten.
Es gibt eine Collection-Schnittstelle in ../packages/db/src/collection.ts. Sie können diese zur Implementierung eigener Collection-Typen verwenden.
Siehe die vorhandenen Implementierungen in ../packages/db, ../packages/query-db-collection, ../packages/electric-db-collection und ../packages/trailbase-db-collection als Referenz.
Verwenden Sie den useLiveQuery Hook, um Live Query Ergebnisse einer Zustandsvariablen in Ihren React-Komponenten zuzuweisen.
import { useLiveQuery } from '@tanstack/react-db'
import { eq } from '@tanstack/db'
const Todos = () => {
const { data: todos } = useLiveQuery((q) =>
q
.from({ todo: todoCollection })
.where(({ todo }) => eq(todo.completed, false))
.orderBy(({ todo }) => todo.created_at, 'asc')
.select(({ todo }) => ({
id: todo.id,
text: todo.text
}))
)
return <List items={ todos } />
}
import { useLiveQuery } from '@tanstack/react-db'
import { eq } from '@tanstack/db'
const Todos = () => {
const { data: todos } = useLiveQuery((q) =>
q
.from({ todo: todoCollection })
.where(({ todo }) => eq(todo.completed, false))
.orderBy(({ todo }) => todo.created_at, 'asc')
.select(({ todo }) => ({
id: todo.id,
text: todo.text
}))
)
return <List items={ todos } />
}
Sie können auch über Collections mit Joins abfragen.
import { useLiveQuery } from '@tanstack/react-db'
import { eq } from '@tanstack/db'
const Todos = () => {
const { data: todos } = useLiveQuery((q) =>
q
.from({ todos: todoCollection })
.join(
{ lists: listCollection },
({ todos, lists }) => eq(lists.id, todos.listId),
'inner'
)
.where(({ lists }) => eq(lists.active, true))
.select(({ todos, lists }) => ({
id: todos.id,
title: todos.title,
listName: lists.name
}))
)
return <List items={ todos } />
}
import { useLiveQuery } from '@tanstack/react-db'
import { eq } from '@tanstack/db'
const Todos = () => {
const { data: todos } = useLiveQuery((q) =>
q
.from({ todos: todoCollection })
.join(
{ lists: listCollection },
({ todos, lists }) => eq(lists.id, todos.listId),
'inner'
)
.where(({ lists }) => eq(lists.active, true))
.select(({ todos, lists }) => ({
id: todos.id,
title: todos.title,
listName: lists.name
}))
)
return <List items={ todos } />
}
Sie können Abfragen auch direkt (außerhalb des Komponentens-Lebenszyklus) mit der zugrundeliegenden queryBuilder API erstellen.
import { createLiveQueryCollection, eq } from "@tanstack/db"
const completedTodos = createLiveQueryCollection({
startSync: true,
query: (q) =>
q
.from({ todo: todoCollection })
.where(({ todo }) => eq(todo.completed, true)),
})
const results = completedTodos.toArray
import { createLiveQueryCollection, eq } from "@tanstack/db"
const completedTodos = createLiveQueryCollection({
startSync: true,
query: (q) =>
q
.from({ todo: todoCollection })
.where(({ todo }) => eq(todo.completed, true)),
})
const results = completedTodos.toArray
Beachten Sie auch, dass
Siehe die Dokumentation zu Live Queries für weitere Details.
Transaktionale Mutatoren ermöglichen es Ihnen, lokale Änderungen über Collections hinweg zu bündeln und zu staged mit
Mutatoren werden mit einem mutationFn erstellt. Sie können eine einzelne, generische mutationFn für Ihre gesamte App definieren. Oder Sie können Collection- oder mutationsspezifische Funktionen definieren.
Die mutationFn ist dafür verantwortlich, die lokalen Änderungen zu verarbeiten und sie zu verarbeiten, normalerweise um sie an einen Server oder eine Datenbank zur Speicherung zu senden.
Wichtig: Innerhalb Ihrer mutationFn müssen Sie sicherstellen, dass Ihre Server-Schreibvorgänge synchronisiert sind, bevor Sie zurückkehren, da der optimistische Zustand verworfen wird, wenn Sie die Mutationsfunktion verlassen. Normalerweise verwenden Sie dazu Collection-spezifische Helfer wie Query's utils.refetch(), direkte Schreib-APIs oder Electric's utils.awaitTxId().
Zum Beispiel
import type { MutationFn } from "@tanstack/react-db"
const mutationFn: MutationFn = async ({ transaction }) => {
const response = await api.todos.create(transaction.mutations)
if (!response.ok) {
// Throwing an error will rollback the optimistic state.
throw new Error(`HTTP Error: ${response.status}`)
}
const result = await response.json()
// Wait for the transaction to be synced back from the server
// before discarding the optimistic state.
const collection: Collection = transaction.mutations[0].collection
await collection.refetch()
}
import type { MutationFn } from "@tanstack/react-db"
const mutationFn: MutationFn = async ({ transaction }) => {
const response = await api.todos.create(transaction.mutations)
if (!response.ok) {
// Throwing an error will rollback the optimistic state.
throw new Error(`HTTP Error: ${response.status}`)
}
const result = await response.json()
// Wait for the transaction to be synced back from the server
// before discarding the optimistic state.
const collection: Collection = transaction.mutations[0].collection
await collection.refetch()
}
Verwenden Sie createOptimisticAction mit Ihren mutationFn- und onMutate-Funktionen, um eine Aktion zu erstellen, die Sie in Ihren Komponenten auf vollständig benutzerdefinierte Weise zur Datenmutation verwenden können.
import { createOptimisticAction } from "@tanstack/react-db"
// Create the `addTodo` action, passing in your `mutationFn` and `onMutate`.
const addTodo = createOptimisticAction<string>({
onMutate: (text) => {
// Instantly applies the local optimistic state.
todoCollection.insert({
id: uuid(),
text,
completed: false,
})
},
mutationFn: async (text, params) => {
// Persist the todo to your backend
const response = await fetch("/api/todos", {
method: "POST",
body: JSON.stringify({ text, completed: false }),
})
const result = await response.json()
// IMPORTANT: Ensure server writes have synced back before returning
// This ensures the optimistic state can be safely discarded
await todoCollection.utils.refetch()
return result
},
})
const Todo = () => {
const handleClick = () => {
// Triggers the onMutate and then the mutationFn
addTodo("🔥 Make app faster")
}
return <Button onClick={handleClick} />
}
import { createOptimisticAction } from "@tanstack/react-db"
// Create the `addTodo` action, passing in your `mutationFn` and `onMutate`.
const addTodo = createOptimisticAction<string>({
onMutate: (text) => {
// Instantly applies the local optimistic state.
todoCollection.insert({
id: uuid(),
text,
completed: false,
})
},
mutationFn: async (text, params) => {
// Persist the todo to your backend
const response = await fetch("/api/todos", {
method: "POST",
body: JSON.stringify({ text, completed: false }),
})
const result = await response.json()
// IMPORTANT: Ensure server writes have synced back before returning
// This ensures the optimistic state can be safely discarded
await todoCollection.utils.refetch()
return result
},
})
const Todo = () => {
const handleClick = () => {
// Triggers the onMutate and then the mutationFn
addTodo("🔥 Make app faster")
}
return <Button onClick={handleClick} />
}
createOptimisticAction ist eine ca. 25 Zeilen lange Funktion, die ein gängiges Transaktionsmuster implementiert. Sie können gerne eigene Muster erfinden! Indem Sie Transaktionen manuell erstellen, können Sie deren Lebenszyklen und Verhaltensweisen vollständig kontrollieren.
Hier ist eine Möglichkeit, wie Sie Transaktionen verwenden könnten.
import { createTransaction } from "@tanstack/react-db"
const addTodoTx = createTransaction({
autoCommit: false,
mutationFn: async ({ transaction }) => {
// Persist data to backend
await Promise.all(transaction.mutations.map(mutation => {
return await api.saveTodo(mutation.modified)
})
},
})
// Apply first change
addTodoTx.mutate(() => todoCollection.insert({ id: '1', text: 'First todo', completed: false }))
// user reviews change
// Apply another change
addTodoTx.mutate(() => todoCollection.insert({ id: '2', text: 'Second todo', completed: false }))
// User decides to save and we call .commit() and the mutations are persisted to the backend.
addTodoTx.commit()
import { createTransaction } from "@tanstack/react-db"
const addTodoTx = createTransaction({
autoCommit: false,
mutationFn: async ({ transaction }) => {
// Persist data to backend
await Promise.all(transaction.mutations.map(mutation => {
return await api.saveTodo(mutation.modified)
})
},
})
// Apply first change
addTodoTx.mutate(() => todoCollection.insert({ id: '1', text: 'First todo', completed: false }))
// user reviews change
// Apply another change
addTodoTx.mutate(() => todoCollection.insert({ id: '2', text: 'Second todo', completed: false }))
// User decides to save and we call .commit() and the mutations are persisted to the backend.
addTodoTx.commit()
Transaktionen durchlaufen die folgenden Zustände
Collections unterstützen insert-, update- und delete-Operationen.
// Insert a single item
myCollection.insert({ text: "Buy groceries", completed: false })
// Insert multiple items
insert([
{ text: "Buy groceries", completed: false },
{ text: "Walk dog", completed: false },
])
// Insert with optimistic updates disabled
myCollection.insert(
{ text: "Server-validated item", completed: false },
{ optimistic: false }
)
// Insert with metadata and optimistic control
myCollection.insert(
{ text: "Custom item", completed: false },
{
metadata: { source: "import" },
optimistic: true, // default behavior
}
)
// Insert a single item
myCollection.insert({ text: "Buy groceries", completed: false })
// Insert multiple items
insert([
{ text: "Buy groceries", completed: false },
{ text: "Walk dog", completed: false },
])
// Insert with optimistic updates disabled
myCollection.insert(
{ text: "Server-validated item", completed: false },
{ optimistic: false }
)
// Insert with metadata and optimistic control
myCollection.insert(
{ text: "Custom item", completed: false },
{
metadata: { source: "import" },
optimistic: true, // default behavior
}
)
Wir verwenden einen Proxy, um Aktualisierungen als unveränderliche Entwurf-optimistische Updates zu erfassen.
// Update a single item
update(todo.id, (draft) => {
draft.completed = true
})
// Update multiple items
update([todo1.id, todo2.id], (drafts) => {
drafts.forEach((draft) => {
draft.completed = true
})
})
// Update with metadata
update(todo.id, { metadata: { reason: "user update" } }, (draft) => {
draft.text = "Updated text"
})
// Update without optimistic updates
update(todo.id, { optimistic: false }, (draft) => {
draft.status = "server-validated"
})
// Update with both metadata and optimistic control
update(
todo.id,
{
metadata: { reason: "admin update" },
optimistic: false,
},
(draft) => {
draft.priority = "high"
}
)
// Update a single item
update(todo.id, (draft) => {
draft.completed = true
})
// Update multiple items
update([todo1.id, todo2.id], (drafts) => {
drafts.forEach((draft) => {
draft.completed = true
})
})
// Update with metadata
update(todo.id, { metadata: { reason: "user update" } }, (draft) => {
draft.text = "Updated text"
})
// Update without optimistic updates
update(todo.id, { optimistic: false }, (draft) => {
draft.status = "server-validated"
})
// Update with both metadata and optimistic control
update(
todo.id,
{
metadata: { reason: "admin update" },
optimistic: false,
},
(draft) => {
draft.priority = "high"
}
)
// Delete a single item
delete todo.id
// Delete multiple items
delete [todo1.id, todo2.id]
// Delete with metadata
delete (todo.id, { metadata: { reason: "completed" } })
// Delete without optimistic updates (waits for server confirmation)
delete (todo.id, { optimistic: false })
// Delete with metadata and optimistic control
delete (todo.id,
{
metadata: { reason: "admin deletion" },
optimistic: false,
})
// Delete a single item
delete todo.id
// Delete multiple items
delete [todo1.id, todo2.id]
// Delete with metadata
delete (todo.id, { metadata: { reason: "completed" } })
// Delete without optimistic updates (waits for server confirmation)
delete (todo.id, { optimistic: false })
// Delete with metadata and optimistic control
delete (todo.id,
{
metadata: { reason: "admin deletion" },
optimistic: false,
})
Standardmäßig wenden alle Mutationen (insert, update, delete) sofort optimistische Updates an, um sofortiges Feedback in Ihrer Benutzeroberfläche zu ermöglichen. Es gibt jedoch Fälle, in denen Sie dieses Verhalten deaktivieren und auf die Serverbestätigung warten möchten, bevor Sie Änderungen lokal anwenden.
Erwägen Sie die Deaktivierung optimistischer Updates, wenn
optimistic: true (Standard):
optimistic: false:
// Example: Critical deletion that needs confirmation
const handleDeleteAccount = () => {
// Don't remove from UI until server confirms
userCollection.delete(userId, { optimistic: false })
}
// Example: Server-generated data
const handleCreateInvoice = () => {
// Server generates invoice number, tax calculations, etc.
invoiceCollection.insert(invoiceData, { optimistic: false })
}
// Example: Mixed approach in same transaction
tx.mutate(() => {
// Instant UI feedback for simple change
todoCollection.update(todoId, (draft) => {
draft.completed = true
})
// Wait for server confirmation for complex change
auditCollection.insert(auditRecord, { optimistic: false })
})
// Example: Critical deletion that needs confirmation
const handleDeleteAccount = () => {
// Don't remove from UI until server confirms
userCollection.delete(userId, { optimistic: false })
}
// Example: Server-generated data
const handleCreateInvoice = () => {
// Server generates invoice number, tax calculations, etc.
invoiceCollection.insert(invoiceData, { optimistic: false })
}
// Example: Mixed approach in same transaction
tx.mutate(() => {
// Instant UI feedback for simple change
todoCollection.update(todoId, (draft) => {
draft.completed = true
})
// Wait for server confirmation for complex change
auditCollection.insert(auditRecord, { optimistic: false })
})
Hier stellen wir zwei gängige Möglichkeiten zur Verwendung von TanStack DB vor
Tipp
Sie können diese Muster kombinieren. Einer der Vorteile von TanStack DB ist, dass Sie verschiedene Wege zum Laden von Daten und zur Handhabung von Mutationen in dieselbe App integrieren können. Ihre Komponenten müssen nicht wissen, woher die Daten stammen oder wohin sie gehen.
Sie können TanStack DB mit Ihrer bestehenden REST-API über TanStack Query verwenden.
Die Schritte sind:
import { useLiveQuery, createCollection } from "@tanstack/react-db"
import { queryCollectionOptions } from "@tanstack/query-db-collection"
// Load data into collections using TanStack Query.
// It's common to define these in a `collections` module.
const todoCollection = createCollection(queryCollectionOptions({
queryKey: ["todos"],
queryFn: async () => fetch("/api/todos"),
getKey: (item) => item.id,
schema: todoSchema, // any standard schema
onInsert: async ({ transaction }) => {
const { changes: newTodo } = transaction.mutations[0]
// Handle the local write by sending it to your API.
await api.todos.create(newTodo)
}
// also add onUpdate, onDelete as needed.
}))
const listCollection = createCollection(queryCollectionOptions({
queryKey: ["todo-lists"],
queryFn: async () => fetch("/api/todo-lists"),
getKey: (item) => item.id,
schema: todoListSchema,
onInsert: async ({ transaction }) => {
const { changes: newTodo } = transaction.mutations[0]
// Handle the local write by sending it to your API.
await api.todoLists.create(newTodo)
}
// also add onUpdate, onDelete as needed.
}))
const Todos = () => {
// Read the data using live queries. Here we show a live
// query that joins across two collections.
const { data: todos } = useLiveQuery((q) =>
q
.from({ todo: todoCollection })
.join(
{ list: listCollection },
({ todo, list }) => eq(list.id, todo.list_id),
'inner'
)
.where(({ list }) => eq(list.active, true))
.select(({ todo, list }) => ({
id: todo.id,
text: todo.text,
status: todo.status,
listName: list.name
}))
)
// ...
}
import { useLiveQuery, createCollection } from "@tanstack/react-db"
import { queryCollectionOptions } from "@tanstack/query-db-collection"
// Load data into collections using TanStack Query.
// It's common to define these in a `collections` module.
const todoCollection = createCollection(queryCollectionOptions({
queryKey: ["todos"],
queryFn: async () => fetch("/api/todos"),
getKey: (item) => item.id,
schema: todoSchema, // any standard schema
onInsert: async ({ transaction }) => {
const { changes: newTodo } = transaction.mutations[0]
// Handle the local write by sending it to your API.
await api.todos.create(newTodo)
}
// also add onUpdate, onDelete as needed.
}))
const listCollection = createCollection(queryCollectionOptions({
queryKey: ["todo-lists"],
queryFn: async () => fetch("/api/todo-lists"),
getKey: (item) => item.id,
schema: todoListSchema,
onInsert: async ({ transaction }) => {
const { changes: newTodo } = transaction.mutations[0]
// Handle the local write by sending it to your API.
await api.todoLists.create(newTodo)
}
// also add onUpdate, onDelete as needed.
}))
const Todos = () => {
// Read the data using live queries. Here we show a live
// query that joins across two collections.
const { data: todos } = useLiveQuery((q) =>
q
.from({ todo: todoCollection })
.join(
{ list: listCollection },
({ todo, list }) => eq(list.id, todo.list_id),
'inner'
)
.where(({ list }) => eq(list.active, true))
.select(({ todo, list }) => ({
id: todo.id,
text: todo.text,
status: todo.status,
listName: list.name
}))
)
// ...
}
Dieses Muster ermöglicht es Ihnen, eine bestehende TanStack Query-Anwendung oder jede andere Anwendung, die auf einer REST-API basiert, mit blitzschnellen, cross-collection Live Queries und lokalen optimistischen Mutationen mit automatisch verwalteten optimistischen Zuständen zu erweitern.
Eine der leistungsfähigsten Arten, TanStack DB zu nutzen, ist mit einer Sync-Engine für ein vollständig lokales First-Erlebnis mit Echtzeit-Sync. Dies ermöglicht es Ihnen, Sync inkrementell in eine bestehende App zu integrieren, während Schreibvorgänge weiterhin mit Ihrer bestehenden API gehandhabt werden.
Hier stellen wir dieses Muster mithilfe von ElectricSQL als Sync-Engine dar.
import type { Collection } from '@tanstack/db'
import type { MutationFn, PendingMutation, createCollection } from '@tanstack/react-db'
import { electricCollectionOptions } from '@tanstack/electric-db-collection'
export const todoCollection = createCollection(electricCollectionOptions({
id: 'todos',
schema: todoSchema,
// Electric syncs data using "shapes". These are filtered views
// on database tables that Electric keeps in sync for you.
shapeOptions: {
url: 'https://api.electric-sql.cloud/v1/shape',
params: {
table: 'todos'
}
},
getKey: (item) => item.id,
schema: todoSchema,
onInsert: async ({ transaction }) => {
const response = await api.todos.create(transaction.mutations[0].modified)
return { txid: response.txid}
}
// You can also implement onUpdate, onDelete as needed.
}))
const AddTodo = () => {
return (
<Button
onClick={() =>
todoCollection.insert({ text: "🔥 Make app faster" })
}
/>
)
}
import type { Collection } from '@tanstack/db'
import type { MutationFn, PendingMutation, createCollection } from '@tanstack/react-db'
import { electricCollectionOptions } from '@tanstack/electric-db-collection'
export const todoCollection = createCollection(electricCollectionOptions({
id: 'todos',
schema: todoSchema,
// Electric syncs data using "shapes". These are filtered views
// on database tables that Electric keeps in sync for you.
shapeOptions: {
url: 'https://api.electric-sql.cloud/v1/shape',
params: {
table: 'todos'
}
},
getKey: (item) => item.id,
schema: todoSchema,
onInsert: async ({ transaction }) => {
const response = await api.todos.create(transaction.mutations[0].modified)
return { txid: response.txid}
}
// You can also implement onUpdate, onDelete as needed.
}))
const AddTodo = () => {
return (
<Button
onClick={() =>
todoCollection.insert({ text: "🔥 Make app faster" })
}
/>
)
}
Bei der Verwendung von TanStack DB mit React Native müssen Sie eine UUID-Generierungsbibliothek installieren und konfigurieren, da React Native crypto.randomUUID() nicht standardmäßig enthält.
Installieren Sie das Paket react-native-random-uuid.
npm install react-native-random-uuid
npm install react-native-random-uuid
Importieren Sie es dann am Einstiegspunkt Ihrer React Native-App (z. B. in Ihrer App.js oder index.js).
import 'react-native-random-uuid'
import 'react-native-random-uuid'
Dieser Polyfill stellt die Funktion crypto.randomUUID() bereit, die TanStack DB intern zur Generierung eindeutiger Bezeichner verwendet.
Wenn Sie Fragen haben / Hilfe bei der Verwendung von TanStack DB benötigen, lassen Sie es uns auf Discord wissen oder starten Sie eine GitHub-Diskussion.
Ihre wöchentliche Dosis JavaScript-Nachrichten. Jeden Montag kostenlos an über 100.000 Entwickler geliefert.