Skip to content

Race condition with updating items in collection in quick succession? #1374

@MichaelMjc

Description

@MichaelMjc

Describe the bug
When multiple items in a queryCollectionOptions-backed collection are updated in rapid succession (e.g. toggling several task checkboxes quickly), each individual onUpdate call fires a server mutation and, upon resolution, triggers a refetch of the underlying queryFn. Because these refetches resolve at different times, a response that arrives after a later optimistic update can carry stale server data that overwrites the still-pending optimistic state — causing UI "flicker" where previously checked items revert to unchecked.

Not sure if this is expected behaviour or maybe there is a config that I'm missing.

To Reproduce
Collection setup (src/db-collections/index.ts):

export const tasksCollection = createCollection(
  queryCollectionOptions({
    queryKey: ['tasks'],
    queryFn: async () => {
      return await listTasks()
    },
    queryClient: getContext().queryClient,
    getKey: (item) => item.id,
    schema: taskSchemas.select,
    onUpdate: async ({ transaction }) => {
      await Promise.all(
        transaction.mutations.map((mutation) =>
          updateTask({
            data: {
              ...mutation.changes,
              id: mutation.original.id,
            },
          }),
        ),
      )
    },
  }),
)

Component usage (src/routes/app/checklist.tsx):

// Live query
const tasks = useLiveSuspenseQuery((q) =>
  q
    .from({ tasks: tasksCollection })
    .orderBy((t) => t.tasks.dueDate, 'desc'),
)
// Per-task toggle
function TaskRow({ task }) {
  const done = task.status === 'done'
  async function toggle() {
    tasksCollection.update(task.id, (draft) => {
      draft.status = done ? 'pending' : 'done'
    })
  }
  // ...
}

Steps

  1. Render a list of tasks backed by queryCollectionOptions with an async onUpdate
  2. Rapidly click the checkbox on 3–5 different tasks within ~500ms of each other
  3. Observe that some tasks that were checked flicker back to unchecked before settling

Expected behavior
I would expect the collection to behave similarly to the pattern TanStack Query recommends for optimistic updates — where in-flight queries are cancelled when a mutation is fired to prevent them from overwriting optimistic state. In TanStack Query you have to do this manually via queryClient.cancelQueries() in onMutate. Since TanStack DB's queryCollectionOptions owns both the mutation lifecycle (onUpdate) and the underlying query (queryFn), I would expect it to automatically cancel or suppress any in-flight or pending refetches for the duration of a transaction — so that a stale server response can never clobber an optimistic update that is still pending.

Additional context
I'm not currently passing any explicit gcTime, staleTime, or mutation-debouncing options to queryCollectionOptions. If there is a config option (e.g. suppressing refetches while transactions are in-flight, a gcTime/staleTime setting, or a way to batch onUpdate calls) that would prevent completed refetches from clobbering pending optimistic state, I'd love to know what to set. I couldn't find guidance on this in the docs.

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