Skip to content

fix(query-core): re-attach MutationObserver to its in-flight mutation on resubscribe - #11217

Closed
iamshahid1997 wants to merge 1 commit into
TanStack:mainfrom
iamshahid1997:fix/mutation-observer-resubscribe
Closed

fix(query-core): re-attach MutationObserver to its in-flight mutation on resubscribe#11217
iamshahid1997 wants to merge 1 commit into
TanStack:mainfrom
iamshahid1997:fix/mutation-observer-resubscribe

Conversation

@iamshahid1997

@iamshahid1997 iamshahid1997 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Fixes #11171

Problem

When the last listener unsubscribes, MutationObserver.onUnsubscribe() removes the observer from its current mutation — but resubscribing never adds it back. React legitimately tears down and re-establishes the store subscription of a mounted component that keeps its state (<Activity mode="hidden">, re-suspending <Suspense> boundaries, StrictMode in dev).

If that happens while a mutation is in flight, the mutation settles against an empty observer list, and the observer keeps serving its frozen pending snapshot from getCurrentResult() forever:

  • isPending stays true → submit buttons using disabled={isPending} stay disabled until remount
  • callbacks passed to mutate(vars, { onSuccess }) never fire
  • devtools/MutationCache show success while the hook shows pending

Fix

Mirror QueryObserver.onSubscribe(): when the first listener subscribes, re-attach the observer to its current mutation (Mutation.addObserver is already idempotent) and refresh the result snapshot in case the mutation settled while detached.

Tests

Two regression tests in mutationObserver.test.tsx, driving subscribe/unsubscribe the way useSyncExternalStore does:

  1. unsubscribe + resubscribe while the mutation is in flight → observer reaches success and notifies the new listener
  2. mutation settles while fully unsubscribed → resubscribing refreshes the snapshot to the final state

Both fail on main and pass with the fix. Full query-core (627) and react-query (577) suites pass.

Summary by CodeRabbit

  • Bug Fixes

    • Mutation status and results now refresh correctly when listeners unsubscribe and later resubscribe.
    • In-progress mutations remain tracked across subscription changes.
    • Mutations that finish while no listeners are present now report their final success state when observed again.
  • Tests

    • Added coverage for mutation observer behavior across unsubscribe and resubscribe cycles.

… on resubscribe

When the last listener unsubscribes, onUnsubscribe() removes the observer
from its current mutation, but resubscribing never added it back. If React
tore down and re-established the subscription while a mutation was in
flight (StrictMode, <Activity> hide/show, re-suspending boundaries), the
mutation settled against an empty observer list and the observer kept
serving its frozen 'pending' snapshot forever.

Mirror QueryObserver.onSubscribe(): when the first listener subscribes,
re-attach to the current mutation and refresh the result in case the
mutation settled while detached.

Fixes TanStack#11171
@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 377ea04e-9e0b-45f6-a739-ef44e9c79882

📥 Commits

Reviewing files that changed from the base of the PR and between 279bb55 and 969cdeb.

📒 Files selected for processing (2)
  • packages/query-core/src/__tests__/mutationObserver.test.tsx
  • packages/query-core/src/mutationObserver.ts

Included review availability: Your plan includes up to 10 reviews per rolling hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

MutationObserver now reattaches to its current mutation and refreshes its result after resubscription. Tests cover mutations completing during and after unsubscribed periods.

Changes

Mutation observer recovery

Layer / File(s) Summary
Resubscription state recovery
packages/query-core/src/mutationObserver.ts, packages/query-core/src/__tests__/mutationObserver.test.tsx
MutationObserver.onSubscribe() reattaches the first listener to the current mutation and refreshes the current result. Tests cover in-flight mutations and mutations that settle while unsubscribed.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 969cd

This localized change re-attaches mutation observation across subscription resubscription and adds regression coverage; no actionable merge-blocking risk remains beyond normal checks and review.

Suggested reviewers: sukvvon

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary fix: reattaching MutationObserver to its in-flight mutation on resubscribe.
Description check ✅ Passed The description explains the problem, fix, regression tests, affected behavior, and test results, but omits the template checklist and release-impact sections.
Linked Issues check ✅ Passed The implementation reattaches the observer and refreshes its result, directly addressing issue #11171 and covering both required resubscription scenarios with tests.
Out of Scope Changes check ✅ Passed The code changes and regression tests are limited to MutationObserver resubscription behavior described in issue #11171.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@TkDodo

TkDodo commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

duplicate of #11172

@TkDodo TkDodo closed this Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants