查询集合可实现 TanStack DB 和 TanStack Query 之间的无缝集成,能够自动同步本地数据库和远程数据源。
@tanstack/query-db-collection
包允许您创建集合,这些集合可以:
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,
})
)
queryCollectionOptions
函数接受以下选项:
您可以定义在发生突变时调用的处理器。这些处理器可以将更改持久化到您的后端,并控制操作后查询是否应重新获取。
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)
}
})
)
默认情况下,在任何持久化处理器(onInsert、onUpdate 或 onDelete)成功完成后,查询将自动重新获取,以确保本地状态与服务器状态匹配。
您可以通过返回一个具有 refetch 属性的对象来控制此行为。
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 }
}
这在以下情况下很有用:
集合通过 collection.utils 提供这些工具方法。
直接写入适用于正常查询/突变流程不适合您需求的场景。它们允许您直接写入同步数据存储,绕过乐观更新系统和查询重新获取机制。
查询集合维护两个数据存储:
常规集合操作(插入、更新、删除)会创建乐观突变,这些突变会:
直接写入完全绕过此系统,直接写入同步数据存储,使其成为处理来自其他来源的实时更新的理想选择。
应在以下情况下使用直接写入:
// 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 })
这些操作:
writeBatch
方法允许您原子地执行多个操作。在回调中调用的任何写入操作都将被收集并作为单个事务执行。
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)
})
})
}
查询集合将 queryFn 的结果视为集合的完整状态。这意味着:
当 queryFn 返回一个空数组时,集合中的所有项都将被删除。这是因为集合将空数组解释为“服务器没有项”。
// This will delete all items in the collection
queryFn: async () => []
// This will delete all items in the collection
queryFn: async () => []
由于查询集合期望 queryFn 返回完整状态,因此您可以通过合并新数据与现有数据来处理部分获取。
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,
})
)
此模式允许您:
直接写入会立即更新集合,同时也会更新 TanStack Query 缓存。但是,它们不会阻止正常的查询同步行为。如果您的 queryFn 返回的数据与您的直接写入冲突,查询数据将优先。
为了正确处理此问题:
所有直接写入方法都可以在 collection.utils 上找到。
您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。