Skip to content

Bug: Mutation refetch in on-demand mode re-runs entire query instead of targeting changed items #821

@RayVentura

Description

@RayVentura

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:

  1. Inefficient network usage: Fetches N items (the entire query result) instead of 1 item (only the changed item)
  2. 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:

  1. Create a collection in on-demand mode with 20 items on the server
  2. Load a live query for "10 items ordered by createdAt desc" → collection gets items 1-10
  3. Update item 6 using collection.update(6, callback)
  4. The onUpdate handler runs and returns { refetch: true }
  5. Refetch executes and re-runs the ORIGINAL query: "order by createdAt desc limit 10"
  6. If time has passed or server returns different data, refetch might return items 11-20
  7. 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 6

Expected 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions