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.tsprotectedonUnsubscribe(): 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'constclient=newQueryClient()letresolveRequestconstobserver=newMutationObserver(client,{mutationFn: ()=>newPromise((resolve)=>{resolveRequest=resolve}),})constunsubscribe=observer.subscribe(()=>{})observer.mutate()awaitnewPromise((r)=>setTimeout(r,0))// let the mutation startunsubscribe()// React tears the subscription down (Activity hide, StrictMode, …)observer.subscribe(()=>{})// …and re-establishes itresolveRequest('ok')// request finishesawaitnewPromise((r)=>setTimeout(r,10))console.log(observer.getCurrentResult().status)// 'pending' ← frozen foreverconsole.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.