Skip to content

useMutation freezes at pending forever when React re-subscribes during an in-flight mutation (<Activity> hide/show) #11171

Description

@YYYasin19

Describe the bug

When React tears down and re-establishes the store subscription of a useMutation hook while a mutation is in flight, the hook's result freezes at status: 'pending' forever — even though the Mutation in the MutationCache completes normally and reaches success.

React legitimately does this unsubscribe/resubscribe cycle for a mounted component that keeps its state: in production via <Activity mode="hidden"> (React 19.2) and re-suspending <Suspense> boundaries, in development via StrictMode. The mutation in the repro is started from a plain event handler — no effects involved.

Consequences for the frozen hook instance:

  • isPending stays true forever → e.g. submit buttons using disabled={isPending} are permanently disabled until the component remounts.
  • Callbacks passed to mutate(variables, { onSuccess, ... }) never fire. (Callbacks defined on useMutation({ onSuccess }) itself do fire, and the mutateAsync promise resolves.)
  • Devtools/MutationCache show the mutation as success with no observers — the UI and the cache permanently disagree.

Root cause: MutationObserver.onUnsubscribe() detaches the observer from the running mutation when the last listener unsubscribes:

// packages/query-core/src/mutationObserver.ts
protected onUnsubscribe(): void {
  if (!this.hasListeners()) {
    this.#currentMutation?.removeObserver(this)
  }
}

but there is no onSubscribe() counterpart that re-attaches it (or refreshes its snapshot) when a listener subscribes again. QueryObserver handles exactly this case by re-adding itself to its query in onSubscribe(). So after resubscribe, the mutation finishes and notifies an empty observer list; the observer's cached #currentResult stays pending, and getCurrentResult() serves that frozen snapshot to useSyncExternalStore forever.

The mechanism can also be shown without React, driving subscribe/unsubscribe the way useSyncExternalStore does:

import { MutationObserver, QueryClient } from '@tanstack/query-core'

const client = new QueryClient()
let resolveRequest
const observer = new MutationObserver(client, {
  mutationFn: () => new Promise((resolve) => { resolveRequest = resolve }),
})

const unsubscribe = observer.subscribe(() => {})
observer.mutate()
await new Promise((r) => setTimeout(r, 0)) // let the mutation start

unsubscribe()                // React tears the subscription down (Activity hide, StrictMode, …)
observer.subscribe(() => {}) // …and re-establishes it

resolveRequest('ok')         // request finishes
await new Promise((r) => setTimeout(r, 10))

console.log(observer.getCurrentResult().status)                 // 'pending'  ← frozen forever
console.log(client.getMutationCache().getAll()[0].state.status) // 'success'

Your minimal, reproducible example

https://codesandbox.io/p/sandbox/9v9nmt

Steps to reproduce

https://codesandbox.io/p/sandbox/9v9nmt

Steps to reproduce

  1. Open the sandbox preview (React 19.2.7, react-query 5.101.4, no StrictMode)
  2. Click "save (3s)" — starts a mutation whose mutationFn resolves after 3 seconds
  3. While it is saving, check the "hide" checkbox — this wraps the form in <Activity mode="hidden">, which unmounts its effects but keeps its state
  4. Wait 3+ seconds so the mutation finishes while hidden
  5. Uncheck "hide"
  6. The hook renders pending forever and the button stays disabled, while "read MutationCache" shows the mutation is success

Control: without steps 3–5 the hook correctly transitions pendingsuccess.

Expected behavior

As a user, I expected the useMutation result to reflect the real state of its mutation (success) once the subscription is re-established — the same way useQuery recovers in the identical hide/show scenario. Instead the hook reports pending forever.

How often does this bug happen?

Every time

Screenshots or Videos

No response

Platform

  • OS: macOS 15
  • Browser: Chrome (any — also reproduces headless in jsdom/Node)

Tanstack Query adapter

react-query

TanStack Query version

v5.101.4 (also reproduced on v5.101.2; the relevant code is unchanged on main as of 2026-08-12)

TypeScript version

v5.9.3

Additional context

This issue was discovered by Fable 5 - I have tested the CodeSandbox and looked at the code; but I didn't write it myself.
Since I think it's still valid and haven't found another workaround, I want to let you know.

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