Übersicht

TanStack DB - Dokumentation

Willkommen 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.

Inhalt

  • Funktionsweise — Verstehen Sie das Entwicklungsmodell von TanStack DB und wie die einzelnen Teile zusammenpassen
  • API Referenz — für die Primitives und Funktionsschnittstellen
  • Anwendungsbeispiele — Beispiele für gängige Nutzungsmuster
  • Mehr Infos — wo Sie Unterstützung und weitere Informationen finden

Funktionsweise

TanStack DB funktioniert durch

tsx
// 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>
  )
}

Definition von Collections

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.

Verwendung von Live Queries

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,

  1. normalisierte Daten in Collections zu laden und sie dann durch Abfragen zu denormalisieren; dies vereinfacht Ihr Backend, da keine speziellen API-Endpunkte benötigt werden, die zu Ihrem Client passen
  2. Daten aus mehreren Quellen zu verknüpfen; z.B. einige Daten aus einer Datenbank zu synchronisieren, andere Daten von einer externen API abzurufen und diese dann zu einem einheitlichen Datenmodell für Ihren Frontend-Code zusammenzufügen

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.

Optimistische Mutationen durchführen

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.

ts
// 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.

Explizite Transaktionen

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.

ts
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()
  },
})

Uni-direktionaler Datenfluss

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.

API Referenz

Collections

Es gibt eine Reihe von integrierten Collection-Typen

  1. QueryCollection zum Laden von Daten in Collections mit TanStack Query
  2. ElectricCollection zum Synchronisieren von Daten in Collections mit ElectricSQL
  3. TrailBaseCollection zum Synchronisieren von Daten in Collections mit TrailBase
  4. LocalStorageCollection für kleine Mengen lokaler Daten, die über Browser-Tabs synchronisiert werden
  5. LocalOnlyCollection für In-Memory-Client-Daten oder UI-Zustand

Sie können auch verwenden

Collection Schemas

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>()).

QueryCollection

TanStack Query ruft Daten über verwaltete Abfragen ab. Verwenden Sie queryCollectionOptions, um Daten mithilfe von TanStack Query in eine Collection zu laden.

ts
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.

ElectricCollection

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.

ts
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

  • shapeOptions — die Electric ShapeStreamOptions, die den Shape definieren, der in die Collection synchronisiert werden soll; dies beinhaltet die
    • url zu Ihrer Sync-Engine und
    • params zur Angabe der zu synchronisierenden Tabelle und beliebiger optionaler where-Klauseln usw.
  • getKey — identifiziert die ID für die Zeilen, die in die Collection synchronisiert werden.

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.

ts
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.

TrailBaseCollection

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.

ts
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

  • recordApi — identifiziert die zu synchronisierende API.
  • getKey — identifiziert die ID für die Records, die in die Collection synchronisiert werden.
  • parse — ordnet (v: Todo[k]) => SelectTodo[k] zu.
  • serialize — ordnet (v: SelectTodo[k]) => Todo[k] zu.

Eine neue Collection beginnt erst mit der Synchronisation, wenn Sie collection.preload() aufrufen oder sie abfragen.

LocalStorageCollection

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.

ts
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

  • storageKey — der localStorage-Schlüssel, unter dem alle Collection-Daten gespeichert werden
  • getKey — identifiziert die ID für Elemente in der Collection

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.

ts
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.

LocalOnlyCollection

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.

ts
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

  • getKey — identifiziert die ID für Elemente in der Collection

Optionale Konfiguration

  • initialData — Array von Elementen zum Befüllen der Collection bei der Erstellung
  • onInsert, onUpdate, onDelete — optionale Mutationshandler für benutzerdefinierte Logik

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.

ts
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.

Abgeleitete Collections

Live Queries geben Collections zurück. Dies ermöglicht es Ihnen, Collections von anderen Collections abzuleiten.

Zum Beispiel

ts
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.

Collection

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.

Live Queries

useLiveQuery Hook

Verwenden Sie den useLiveQuery Hook, um Live Query Ergebnisse einer Zustandsvariablen in Ihren React-Komponenten zuzuweisen.

ts
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.

ts
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 } />
}

queryBuilder

Sie können Abfragen auch direkt (außerhalb des Komponentens-Lebenszyklus) mit der zugrundeliegenden queryBuilder API erstellen.

ts
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

  1. die Abfrageergebnisse selbst eine Collection sind
  2. der useLiveQuery automatisch Live-Query-Abos startet und stoppt, wenn Sie Ihre Komponenten mounten und unmounten; wenn Sie Abfragen manuell erstellen, müssen Sie den Abonnement-Lebenszyklus selbst verwalten.

Siehe die Dokumentation zu Live Queries für weitere Details.

Transaktionale Mutatoren

Transaktionale Mutatoren ermöglichen es Ihnen, lokale Änderungen über Collections hinweg zu bündeln und zu staged mit

  • sofortiger Anwendung lokaler optimistischer Updates
  • flexiblen mutationFns zur Behandlung von Schreibvorgängen, mit automatischem Rollback und Management von optimistischem Zustand

mutationFn

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

tsx
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()
}

createOptimisticAction

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.

tsx
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} />
}

Manuelle Transaktionen

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.

ts
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()

Transaktions-Lifecycle

Transaktionen durchlaufen die folgenden Zustände

  1. pending: Anfangszustand, wenn eine Transaktion erstellt wird und optimistische Mutationen angewendet werden können
  2. persisting: Die Transaktion wird an das Backend übertragen
  3. completed: Die Transaktion wurde erfolgreich übertragen und alle Backend-Änderungen wurden zurücksynchronisiert.
  4. failed: Ein Fehler wurde beim Übertragen oder Zurücksynchronisieren der Transaktion ausgelöst.

Schreiboperationen

Collections unterstützen insert-, update- und delete-Operationen.

insert
typescript
// 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
  }
)
update

Wir verwenden einen Proxy, um Aktualisierungen als unveränderliche Entwurf-optimistische Updates zu erfassen.

typescript
// 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
typescript
// 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,
})

Kontrolle des optimistischen Verhaltens

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.

Wann man optimistic: false verwendet

Erwägen Sie die Deaktivierung optimistischer Updates, wenn

  • Komplexe serverseitige Verarbeitung: Einfügungen, die auf serverseitiger Generierung basieren (z. B. kaskadierende Fremdschlüssel, berechnete Felder).
  • Validierungsanforderungen: Operationen, bei denen die Backend-Validierung die Änderung ablehnen könnte.
  • Bestätigungsworkflows: Löschungen, bei denen die Benutzeroberfläche auf eine Bestätigung warten sollte, bevor Daten entfernt werden.
  • Batch-Operationen: Große Operationen, bei denen ein optimistisches Rollback störend wäre.
Verhaltensunterschiede

optimistic: true (Standard):

  • Wendet die Mutation sofort auf den lokalen Store an.
  • Bietet sofortiges UI-Feedback.
  • Erfordert ein Rollback, wenn der Server die Mutation ablehnt.
  • Am besten für einfache, vorhersagbare Operationen.

optimistic: false:

  • Modifiziert den lokalen Store erst, wenn der Server bestätigt.
  • Kein sofortiges UI-Feedback, aber kein Rollback erforderlich.
  • UI-Updates erst nach erfolgreicher Serverantwort.
  • Am besten für komplexe oder validierungsintensive Operationen.
typescript
// 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 })
})

Anwendungsbeispiele

Hier stellen wir zwei gängige Möglichkeiten zur Verwendung von TanStack DB vor

  1. Verwendung von TanStack Query mit einer bestehenden REST-API
  2. Verwendung der ElectricSQL Sync-Engine mit einem generischen Ingestionsendpunkt

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.

1. TanStack Query

Sie können TanStack DB mit Ihrer bestehenden REST-API über TanStack Query verwenden.

Die Schritte sind:

  1. Erstellen von QueryCollections, die Daten mithilfe von TanStack Query laden
  2. Implementieren von mutationFns, die Mutationen durch das Senden an Ihre API-Endpunkte verarbeiten
tsx
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.

2. ElectricSQL Sync

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.

tsx
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" })
      }
    />
  )
}

React Native

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.

bash
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).

javascript
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.

Mehr Infos

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.

Unsere Partner
Code Rabbit
Electric
Prisma
Bytes abonnieren

Ihre wöchentliche Dosis JavaScript-Nachrichten. Jeden Montag kostenlos an über 100.000 Entwickler geliefert.

Bytes

Kein Spam. Jederzeit kündbar.

Bytes abonnieren

Ihre wöchentliche Dosis JavaScript-Nachrichten. Jeden Montag kostenlos an über 100.000 Entwickler geliefert.

Bytes

Kein Spam. Jederzeit kündbar.