Query-Sammlungen bieten eine nahtlose Integration zwischen TanStack DB und TanStack Query und ermöglichen die automatische Synchronisierung zwischen Ihrer lokalen Datenbank und entfernten Datenquellen.
Das Paket @tanstack/query-db-collection ermöglicht es Ihnen, Sammlungen zu erstellen, die
npm install @tanstack/query-db-collection @tanstack/query-core @tanstack/db
npm install @tanstack/query-db-collection @tanstack/query-core @tanstack/db
import { QueryClient } from '@tanstack/query-core'
import { createCollection } from '@tanstack/db'
import { queryCollectionOptions } from '@tanstack/query-db-collection'
const queryClient = new QueryClient()
const todosCollection = createCollection(
queryCollectionOptions({
queryKey: ['todos'],
queryFn: async () => {
const response = await fetch('/api/todos')
return response.json()
},
queryClient,
getKey: (item) => item.id,
})
)
import { QueryClient } from '@tanstack/query-core'
import { createCollection } from '@tanstack/db'
import { queryCollectionOptions } from '@tanstack/query-db-collection'
const queryClient = new QueryClient()
const todosCollection = createCollection(
queryCollectionOptions({
queryKey: ['todos'],
queryFn: async () => {
const response = await fetch('/api/todos')
return response.json()
},
queryClient,
getKey: (item) => item.id,
})
)
Die Funktion queryCollectionOptions akzeptiert die folgenden Optionen
Sie können Handler definieren, die bei Mutationen aufgerufen werden. Diese Handler können Änderungen an Ihrem Backend persistieren und steuern, ob die Abfrage nach der Operation erneut abgerufen werden soll
const todosCollection = createCollection(
queryCollectionOptions({
queryKey: ['todos'],
queryFn: fetchTodos,
queryClient,
getKey: (item) => item.id,
onInsert: async ({ transaction }) => {
const newItems = transaction.mutations.map(m => m.modified)
await api.createTodos(newItems)
// Returning nothing or { refetch: true } will trigger a refetch
// Return { refetch: false } to skip automatic refetch
},
onUpdate: async ({ transaction }) => {
const updates = transaction.mutations.map(m => ({
id: m.key,
changes: m.changes
}))
await api.updateTodos(updates)
},
onDelete: async ({ transaction }) => {
const ids = transaction.mutations.map(m => m.key)
await api.deleteTodos(ids)
}
})
)
const todosCollection = createCollection(
queryCollectionOptions({
queryKey: ['todos'],
queryFn: fetchTodos,
queryClient,
getKey: (item) => item.id,
onInsert: async ({ transaction }) => {
const newItems = transaction.mutations.map(m => m.modified)
await api.createTodos(newItems)
// Returning nothing or { refetch: true } will trigger a refetch
// Return { refetch: false } to skip automatic refetch
},
onUpdate: async ({ transaction }) => {
const updates = transaction.mutations.map(m => ({
id: m.key,
changes: m.changes
}))
await api.updateTodos(updates)
},
onDelete: async ({ transaction }) => {
const ids = transaction.mutations.map(m => m.key)
await api.deleteTodos(ids)
}
})
)
Standardmäßig wird nach Abschluss eines Persistenz-Handlers (onInsert, onUpdate oder onDelete) erfolgreich die Abfrage automatisch erneut abgerufen, um sicherzustellen, dass der lokale Zustand dem Serverzustand entspricht.
Sie können dieses Verhalten steuern, indem Sie ein Objekt mit einer refetch-Eigenschaft zurückgeben
onInsert: async ({ transaction }) => {
await api.createTodos(transaction.mutations.map(m => m.modified))
// Skip the automatic refetch
return { refetch: false }
}
onInsert: async ({ transaction }) => {
await api.createTodos(transaction.mutations.map(m => m.modified))
// Skip the automatic refetch
return { refetch: false }
}
Dies ist nützlich, wenn
Die Sammlung stellt diese Dienstprogramme über collection.utils bereit
Direkte Schreibvorgänge sind für Szenarien gedacht, in denen der normale Abfrage-/Mutationsfluss nicht Ihren Anforderungen entspricht. Sie ermöglichen es Ihnen, direkt in den synchronisierten Datenspeicher zu schreiben, wodurch das System für optimistische Updates und der Abfrage-Refetch-Mechanismus umgangen werden.
Query-Sammlungen unterhalten zwei Datenspeicher
Normale Sammlungsoperationen (Einfügen, Aktualisieren, Löschen) erstellen optimistische Mutationen, die
Direkte Schreibvorgänge umgehen dieses System vollständig und schreiben direkt in den synchronisierten Datenspeicher, was sie ideal für die Verarbeitung von Echtzeitaktualisierungen aus alternativen Quellen macht.
Direkte Schreibvorgänge sollten verwendet werden, wenn
// Insert a new item directly to the synced data store
todosCollection.utils.writeInsert({ id: '1', text: 'Buy milk', completed: false })
// Update an existing item in the synced data store
todosCollection.utils.writeUpdate({ id: '1', completed: true })
// Delete an item from the synced data store
todosCollection.utils.writeDelete('1')
// Upsert (insert or update) in the synced data store
todosCollection.utils.writeUpsert({ id: '1', text: 'Buy milk', completed: false })
// Insert a new item directly to the synced data store
todosCollection.utils.writeInsert({ id: '1', text: 'Buy milk', completed: false })
// Update an existing item in the synced data store
todosCollection.utils.writeUpdate({ id: '1', completed: true })
// Delete an item from the synced data store
todosCollection.utils.writeDelete('1')
// Upsert (insert or update) in the synced data store
todosCollection.utils.writeUpsert({ id: '1', text: 'Buy milk', completed: false })
Diese Operationen
Die Methode writeBatch ermöglicht es Ihnen, mehrere Operationen atomar durchzuführen. Alle Schreiboperationen, die innerhalb des Callbacks aufgerufen werden, werden gesammelt und als eine einzige Transaktion ausgeführt
todosCollection.utils.writeBatch(() => {
todosCollection.utils.writeInsert({ id: '1', text: 'Buy milk' })
todosCollection.utils.writeInsert({ id: '2', text: 'Walk dog' })
todosCollection.utils.writeUpdate({ id: '3', completed: true })
todosCollection.utils.writeDelete('4')
})
todosCollection.utils.writeBatch(() => {
todosCollection.utils.writeInsert({ id: '1', text: 'Buy milk' })
todosCollection.utils.writeInsert({ id: '2', text: 'Walk dog' })
todosCollection.utils.writeUpdate({ id: '3', completed: true })
todosCollection.utils.writeDelete('4')
})
// Handle real-time updates from WebSocket without triggering full refetches
ws.on('todos:update', (changes) => {
todosCollection.utils.writeBatch(() => {
changes.forEach(change => {
switch (change.type) {
case 'insert':
todosCollection.utils.writeInsert(change.data)
break
case 'update':
todosCollection.utils.writeUpdate(change.data)
break
case 'delete':
todosCollection.utils.writeDelete(change.id)
break
}
})
})
})
// Handle real-time updates from WebSocket without triggering full refetches
ws.on('todos:update', (changes) => {
todosCollection.utils.writeBatch(() => {
changes.forEach(change => {
switch (change.type) {
case 'insert':
todosCollection.utils.writeInsert(change.data)
break
case 'update':
todosCollection.utils.writeUpdate(change.data)
break
case 'delete':
todosCollection.utils.writeDelete(change.id)
break
}
})
})
})
// Handle server responses after mutations without full refetch
const createTodo = async (todo) => {
// Optimistically add the todo
const tempId = crypto.randomUUID()
todosCollection.insert({ ...todo, id: tempId })
try {
// Send to server
const serverTodo = await api.createTodo(todo)
// Sync the server response (with server-generated ID and timestamps)
// without triggering a full collection refetch
todosCollection.utils.writeBatch(() => {
todosCollection.utils.writeDelete(tempId)
todosCollection.utils.writeInsert(serverTodo)
})
} catch (error) {
// Rollback happens automatically
throw error
}
}
// Handle server responses after mutations without full refetch
const createTodo = async (todo) => {
// Optimistically add the todo
const tempId = crypto.randomUUID()
todosCollection.insert({ ...todo, id: tempId })
try {
// Send to server
const serverTodo = await api.createTodo(todo)
// Sync the server response (with server-generated ID and timestamps)
// without triggering a full collection refetch
todosCollection.utils.writeBatch(() => {
todosCollection.utils.writeDelete(tempId)
todosCollection.utils.writeInsert(serverTodo)
})
} catch (error) {
// Rollback happens automatically
throw error
}
}
// Load additional pages without refetching existing data
const loadMoreTodos = async (page) => {
const newTodos = await api.getTodos({ page, limit: 50 })
// Add new items without affecting existing ones
todosCollection.utils.writeBatch(() => {
newTodos.forEach(todo => {
todosCollection.utils.writeInsert(todo)
})
})
}
// Load additional pages without refetching existing data
const loadMoreTodos = async (page) => {
const newTodos = await api.getTodos({ page, limit: 50 })
// Add new items without affecting existing ones
todosCollection.utils.writeBatch(() => {
newTodos.forEach(todo => {
todosCollection.utils.writeInsert(todo)
})
})
}
Die Query-Sammlung behandelt das Ergebnis der queryFn als den **vollständigen Zustand** der Sammlung. Das bedeutet
Wenn queryFn ein leeres Array zurückgibt, werden **alle Elemente in der Sammlung gelöscht**. Dies liegt daran, dass die Sammlung ein leeres Array als "der Server hat keine Elemente" interpretiert.
// This will delete all items in the collection
queryFn: async () => []
// This will delete all items in the collection
queryFn: async () => []
Da die Query-Sammlung erwartet, dass queryFn den vollständigen Zustand zurückgibt, können Sie partielle Abrufe verarbeiten, indem Sie neue Daten mit vorhandenen Daten zusammenführen
const todosCollection = createCollection(
queryCollectionOptions({
queryKey: ['todos'],
queryFn: async ({ queryKey }) => {
// Get existing data from cache
const existingData = queryClient.getQueryData(queryKey) || []
// Fetch only new/updated items (e.g., changes since last sync)
const lastSyncTime = localStorage.getItem('todos-last-sync')
const newData = await fetch(`/api/todos?since=${lastSyncTime}`).then(r => r.json())
// Merge new data with existing data
const existingMap = new Map(existingData.map(item => [item.id, item]))
// Apply updates and additions
newData.forEach(item => {
existingMap.set(item.id, item)
})
// Handle deletions if your API provides them
if (newData.deletions) {
newData.deletions.forEach(id => existingMap.delete(id))
}
// Update sync time
localStorage.setItem('todos-last-sync', new Date().toISOString())
// Return the complete merged state
return Array.from(existingMap.values())
},
queryClient,
getKey: (item) => item.id,
})
)
const todosCollection = createCollection(
queryCollectionOptions({
queryKey: ['todos'],
queryFn: async ({ queryKey }) => {
// Get existing data from cache
const existingData = queryClient.getQueryData(queryKey) || []
// Fetch only new/updated items (e.g., changes since last sync)
const lastSyncTime = localStorage.getItem('todos-last-sync')
const newData = await fetch(`/api/todos?since=${lastSyncTime}`).then(r => r.json())
// Merge new data with existing data
const existingMap = new Map(existingData.map(item => [item.id, item]))
// Apply updates and additions
newData.forEach(item => {
existingMap.set(item.id, item)
})
// Handle deletions if your API provides them
if (newData.deletions) {
newData.deletions.forEach(id => existingMap.delete(id))
}
// Update sync time
localStorage.setItem('todos-last-sync', new Date().toISOString())
// Return the complete merged state
return Array.from(existingMap.values())
},
queryClient,
getKey: (item) => item.id,
})
)
Dieses Muster ermöglicht es Ihnen
Direkte Schreibvorgänge aktualisieren die Sammlung sofort und aktualisieren auch den TanStack Query Cache. Sie verhindern jedoch nicht das normale Abfrage-Synchronisierungsverhalten. Wenn Ihre queryFn Daten zurückgibt, die mit Ihren direkten Schreibvorgängen in Konflikt stehen, haben die Abfragedaten Vorrang.
Um dies ordnungsgemäß zu handhaben
Alle direkten Schreibmethoden sind auf collection.utils verfügbar
Ihre wöchentliche Dosis JavaScript-Nachrichten. Jeden Montag kostenlos an über 100.000 Entwickler geliefert.