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
- Open the sandbox preview (React 19.2.7, react-query 5.101.4, no StrictMode)
- Click "save (3s)" — starts a mutation whose
mutationFn resolves after 3 seconds
- While it is saving, check the "hide" checkbox — this wraps the form in
<Activity mode="hidden">, which unmounts its effects but keeps its state
- Wait 3+ seconds so the mutation finishes while hidden
- Uncheck "hide"
- 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 pending → success.
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.
Describe the bug
When React tears down and re-establishes the store subscription of a
useMutationhook while a mutation is in flight, the hook's result freezes atstatus: 'pending'forever — even though theMutationin theMutationCachecompletes normally and reachessuccess.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:
isPendingstaystrueforever → e.g. submit buttons usingdisabled={isPending}are permanently disabled until the component remounts.mutate(variables, { onSuccess, ... })never fire. (Callbacks defined onuseMutation({ onSuccess })itself do fire, and themutateAsyncpromise resolves.)MutationCacheshow the mutation assuccesswith 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:but there is no
onSubscribe()counterpart that re-attaches it (or refreshes its snapshot) when a listener subscribes again.QueryObserverhandles exactly this case by re-adding itself to its query inonSubscribe(). So after resubscribe, the mutation finishes and notifies an empty observer list; the observer's cached#currentResultstayspending, andgetCurrentResult()serves that frozen snapshot touseSyncExternalStoreforever.The mechanism can also be shown without React, driving
subscribe/unsubscribethe wayuseSyncExternalStoredoes:Your minimal, reproducible example
https://codesandbox.io/p/sandbox/9v9nmt
Steps to reproduce
https://codesandbox.io/p/sandbox/9v9nmt
Steps to reproduce
mutationFnresolves after 3 seconds<Activity mode="hidden">, which unmounts its effects but keeps its statependingforever and the button stays disabled, while "read MutationCache" shows the mutation issuccessControl: without steps 3–5 the hook correctly transitions
pending→success.Expected behavior
As a user, I expected the
useMutationresult to reflect the real state of its mutation (success) once the subscription is re-established — the same wayuseQueryrecovers in the identical hide/show scenario. Instead the hook reportspendingforever.How often does this bug happen?
Every time
Screenshots or Videos
No response
Platform
Tanstack Query adapter
react-query
TanStack Query version
v5.101.4 (also reproduced on v5.101.2; the relevant code is unchanged on
mainas 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.