-
Notifications
You must be signed in to change notification settings - Fork 130
Description
Checklist
- I've validated the bug against the latest version of DB packages
Describe the bug
When using a Query Collection in on-demand mode, mutations trigger a refetch that re-runs the entire original query (with orderBy + limit) instead of targeting the specific item that was changed. When this refetch completes, it overwrites ALL collection data with the new query result, causing two critical issues:
- Inefficient network usage: Fetches N items (the entire query result) instead of 1 item (only the changed item)
- Data loss: Items that were previously loaded but aren't in the new query result get removed from the collection, even though they were only updated, not deleted
The root cause is that refetch receives the original query parameters (orderBy, limit) instead of a targeted WHERE clause for the mutated item's ID.
To Reproduce
Steps to reproduce the behavior:
- Create a collection in on-demand mode with 20 items on the server
- Load a live query for "10 items ordered by createdAt desc" → collection gets items 1-10
- Update item 6 using
collection.update(6, callback) - The onUpdate handler runs and returns
{ refetch: true } - Refetch executes and re-runs the ORIGINAL query: "order by createdAt desc limit 10"
- If time has passed or server returns different data, refetch might return items 11-20
- The collection now contains items 11-20, and item 6 is gone
import { QueryClient } from '@tanstack/react-query'
import { createCollection, createLiveQueryCollection, parseLoadSubsetOptions } from '@tanstack/react-db'
import { queryCollectionOptions } from '@tanstack/query-db-collection'
interface Item {
id: number
createdAt: number
value?: string
}
let items: Item[] = Array.from({ length: 20 }, (_, i) => ({ id: i+1, createdAt: Date.now()+i*1000 }))
const runs: any[] = []
const collection = createCollection(queryCollectionOptions({
id: 'items',
queryClient: new QueryClient({ defaultOptions: { queries: { retry: false } } }),
syncMode: 'on-demand',
queryKey: ['items'],
getKey: (i: Item) => i.id,
queryFn: async (ctx) => {
const { limit, where, orderBy } = ctx.meta.loadSubsetOptions
const parsed = parseLoadSubsetOptions({ where, orderBy, limit })
runs.push(parsed || {})
// Simulate "time passing" - second query returns different items
let offset = runs.length === 1 ? 0 : 10
return items.slice(offset, offset+10)
},
onUpdate: async (data) => {
const modified = data.transaction.mutations[0].modified
items[modified.id - 1] = modified
return { refetch: true }
}
}))
const query = createLiveQueryCollection((q) =>
q.from({ item: collection }).orderBy(({ item }) => item.createdAt, 'desc').limit(10)
)
await query.preload()
console.log('State after preload:', Array.from(collection.values()).map(i => i.id))
// Output: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log('Updating item 6')
await collection.update(6, (d) => d.value = 'test').isPersisted.promise
console.log('State after update:', Array.from(collection.values()).map(i => i.id))
// Output: [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
// ❌ Item 1-10, disappeared! including modified item 6Expected behavior
After updating an item, the refetch should call queryFn with a targeted WHERE clause:
{
where: { type: 'eq', args: [{ path: ['id'] }, { value: 6 }] }
// NO orderBy, NO limit
}This would:
- Fetch only item 6 from the server to verify its state
- Update item 6 in the collection with server data
- Leave other previously loaded items (1-5, 7-10) unchanged
- NOT remove items that weren't deleted
- Only remove items if they're actually deleted via
collection.delete()
This is the standard pattern in data fetching: after a mutation, verify the specific item that changed, not re-run the entire query.
Screenshots
N/A - use the reproduction code above
Desktop (please complete the following information):
- OS: Linux
- Browser: Bun
"dependencies": {
"@tanstack/db": "^0.5.0",
"@tanstack/query-db-collection": "^1.0.0",
"@tanstack/react-db": "^0.1.44",
"@tanstack/react-query": "^5.17.9"
}
Additional context
This issue is particularly problematic for:
- Time-based queries: "Last 10 items" where items age out over time
- Filtered queries: Update an item, it refetches with filters that got the item in the first place, item no longer found