Skip to content

useMutationState never removes mutations that no longer match the filter (svelte-query) #11152

Description

@alex-js-ltd

Describe the bug

useMutationState() in @tanstack/svelte-query never shrinks its returned array. Once a mutation stops matching the given filters (e.g. it settles and no longer matches status: 'pending', or it's removed from the mutation cache), the stale entry stays in the returned array forever instead of being dropped.

Root cause: in packages/svelte-query/src/useMutationState.svelte.ts, the cache-subscription callback updates the $state array in place with:

Object.assign(result, nextResult)

Array.prototype.length is a non-enumerable own property, so Object.assign copies the numeric indices present on nextResult but never adjusts result.length. When nextResult is shorter than result (fewer mutations match now than before), the extra trailing entries in result are simply never touched — they survive with their old, stale values and the array never shrinks.

Your minimal, reproducible example

N/A — this reproduces deterministically inside the package's own Vitest suite (jsdom), no external sandbox needed. See "Steps to reproduce" for the exact test to drop in.

Steps to reproduce

Add the following test to packages/svelte-query/tests/useMutationState/useMutationState.svelte.test.ts and run pnpm nx run @tanstack/svelte-query:test:lib -- useMutationState:

it('should remove mutations that no longer match the filter', async () => {
  const firstKey = queryKey()
  const secondKey = queryKey()
  const mutationFn = vi.fn(() => sleep(10).then(() => 'data'))

  const rendered = render(Base, {
    props: {
      queryClient,
      successMutationOpts: () => ({
        mutationKey: firstKey,
        mutationFn,
      }),
      errorMutationOpts: () => ({
        mutationKey: secondKey,
        mutationFn,
      }),
      mutationStateOpts: {
        filters: { status: 'pending' },
      },
    },
  })

  fireEvent.click(rendered.getByRole('button', { name: /Success/i }))
  fireEvent.click(rendered.getByRole('button', { name: /Error/i }))

  await vi.advanceTimersByTimeAsync(0)

  expect(
    rendered.getByText('Data: ["pending","pending"]'),
  ).toBeInTheDocument()

  await vi.advanceTimersByTimeAsync(10)

  expect(rendered.getByText('Data: []')).toBeInTheDocument()
})
  1. Two mutations are started concurrently, both matching filters: { status: 'pending' } → the rendered output correctly shows Data: ["pending","pending"].
  2. Both mutations settle (10ms later), so zero mutations now match status: 'pending'.
  3. The rendered output is still Data: ["pending","pending"] instead of Data: [] — the last assertion fails.

Any scenario that shrinks the matched set reproduces this: a narrower filter, a mutation being garbage-collected, or mutationCache.clear().

Expected behavior

As a user, I expected useMutationState()'s returned array to always reflect exactly the mutations currently matching filters (as it does in @tanstack/react-query, @tanstack/vue-query, and @tanstack/solid-query), but instead it only ever grows — mutations that stop matching are never removed.

How often does this bug happen?

Every time

Platform

  • OS: N/A (reproducible in any environment)
  • Browser: N/A — reproduces headlessly via jsdom in the package's Vitest suite
  • Version: N/A

Tanstack Query adapter

svelte-query

TanStack Query version

v6.1.38

TypeScript version

v5.9.3

Additional context

The bug was introduced in the Svelte 5 runes rewrite (#9694) and has been present since — the file hasn't been touched since that commit. There's also a leftover commented-out block in the same file showing an earlier (React-style) result.current = nextResult approach that would not have had this problem, apparently abandoned mid-port in favor of the buggy Object.assign mutation.

Suggested fix (one line, keeps the $state array's reference identity intact, which the rune requires):

- Object.assign(result, nextResult)
+ result.splice(0, result.length, ...nextResult)

I have a fix + regression test ready and will open a PR referencing this issue.

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