Skip to content

fix(query-core): reconnect MutationObserver on resubscribe (StrictMode fix) - #11120

Closed
leonardoag2026-beep wants to merge 5 commits into
TanStack:mainfrom
leonardoag2026-beep:fix/mutation-observer-resubscribe-strictmode
Closed

fix(query-core): reconnect MutationObserver on resubscribe (StrictMode fix)#11120
leonardoag2026-beep wants to merge 5 commits into
TanStack:mainfrom
leonardoag2026-beep:fix/mutation-observer-resubscribe-strictmode

Conversation

@leonardoag2026-beep

@leonardoag2026-beepleonardoag2026-beep commented Jul 26, 2026

Copy link
Copy Markdown

Look, this is a pr related to this thread #5341.

In the arguments, I saw "that mutations shouldn't have side-effects", "Strict mode tell us we shouldnt use mutate inside use effects", I do understand that "Only on dev happens, but on production it works", I do understand that "useMutationState solves the business" and I also get that "You don't want to waste your time having a workaround to trick StrictMode".

but hey, listen to me. In regards of you not wanting to waste your time, its fine, because this pr solves the business. In regard to using useMutationState, I argue that on a huge codebase, in my opinion it's pretty messy to call useMutationState, because first, isPending should just work regardless of whether you used useEffect or not. isPending should still work reliably on react, without this useMutationState, which in my opinion its only useful for tricking the StrictMode.

I know, that only on dev mode, this issue happens, but listen to me broyos, if i'm developing, i'm gonna test on develop mode, i'm not gonna build to prod everytime I need to make it work.

In regards to "mutations shouldn't have side effects", I argue that Tanstack Query Started as React Query, Tanstack is react's bitch, Bow to it (hey if you didn't laugh I meant no harm, just trying to be funny guy, you dont need to bow if you think the code gets messy).

Now about where the fix lives: onSubscribe() is on query-core, called by Subscribable.subscribe(). It fires for ALL frameworks (React, Solid, Vue, Svelte...). But the scenario that breaks (subscribe → unsubscribe → subscribe on the same tick) is exclusive to React's StrictMode. For every other framework:

  • If #currentMutation is undefined → onSubscribe does nothing
  • If #currentMutation exists → addObserver reconnects (harmless, just adds it back)

So the fix on query-core is safe for everyone. Moving it to react-query wouldn't work because MutationObserver lives on query-core and onSubscribe is a protected method called from inside the class.

But if you still think the change should live only on react-query, I can figure out a way. At least react query would be react's bitch and bow to it, right?


Changes

Adds onSubscribe() to MutationObserver in query-core. When a new listener subscribes and there is an existing #currentMutation, the observer is reconnected to it via addObserver(this), its result is refreshed via #updateResult(), and subscribers are notified via #notify(). This mirrors the existing pattern in QueryObserver.onSubscribe().

Checklist

  • Tests pass (58/58, including mutationObserver, mutations, mutationCache)
  • Appropriate type coverage — no new types added
  • No breaking changes — only adds a hook that's a no-op when #currentMutation is undefined

Release Impact

  • Patch — bug fix, no breaking changes

Summary by CodeRabbit

  • Bug Fixes

    • Fixed mutation status handling when listeners unsubscribe and resubscribe during an in-progress mutation.
    • Mutation observers now reconnect correctly and report the eventual success state and result data instead of remaining stuck in a pending state.
  • Tests

    • Added coverage for unsubscribe and resubscribe behavior during active mutations.

…e fix)
Add onSubscribe() to MutationObserver to reconnect the observer to an
in-progress mutation when the component resubscribes (e.g. React's
StrictMode double-mount cycle). Without this, the observer is removed
from the mutation's observers list on cleanup and never reconnects,
causing isPending to stay true indefinitely.
QueryObserver already has the same pattern — this makes MutationObserver
consistent with it.
@coderabbitai

coderabbitaiBot commented Jul 26, 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: 32fd2f51-88f8-4711-a1f7-a0f22dc72a80

📥 Commits

Reviewing files that changed from the base of the PR and between 1ef4208 and 3cee7e6.

📒 Files selected for processing (3)
  • .changeset/mutation-observer-resubscribe.md
  • packages/query-core/src/__tests__/mutationObserver.test.tsx
  • packages/query-core/src/mutationObserver.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/query-core/src/tests/mutationObserver.test.tsx
  • .changeset/mutation-observer-resubscribe.md
  • packages/query-core/src/mutationObserver.ts

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


📝 Walkthrough

Walkthrough

MutationObserver.onSubscribe reconnects to an existing mutation, refreshes its result, and notifies subscribers. A regression test covers resubscription during an in-flight mutation. A patch changeset documents the fix.

Changes

Mutation observer lifecycle

Layer / File(s)Summary
Wire mutation subscriptions
packages/query-core/src/mutationObserver.ts, packages/query-core/src/__tests__/mutationObserver.test.tsx, .changeset/mutation-observer-resubscribe.md
onSubscribe reconnects the observer to the current mutation, updates the result, and notifies subscribers. The test verifies the eventual success result and data after resubscription. The changeset documents the patch fix.

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

Merge Risk:⚪ Minimal · up to 3cee7

This localized fix reconnects mutation observers during resubscription and includes targeted tests and release metadata; no actionable merge-blocking risk remains beyond normal checks and review.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly and concisely describes the MutationObserver resubscription fix and its StrictMode context.
Description check✅ PassedThe description explains the change, motivation, testing, compatibility, and patch release impact, but it does not use all template checklist items.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ 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.

MILLERMARRU

This comment was marked as spam.

…e===1
Apply Miller Marru's review suggestion: only reattach the observer to the
current mutation when the first listener subscribes (listeners.size === 1),
matching QueryObserver.onSubscribe's pattern exactly.
Add regression test proving the observer reconnects to an in-flight
mutation on resubscribe (StrictMode double-mount cycle). Verified RED on
main (test fails without onSubscribe) and GREEN with the fix.

@leonardoag2026-beepleonardoag2026-beep left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right, we need a guard with listeners.size === 1. onSubscribe is called on every subscribe(), so it's not just the first time. Without this, an observer already connected would have to reconnect, which would be a waste of time. Pushed b849bce with the guard. Now MutationObserver behaves in this respect the same way as QueryObserver.

I also added regression testing to StrictMode. It unsubscribes while the mutation is still in progress, and then resubscribes to see if the observer finally succeeds. First, I tried to run it on the main branch, made sure it really failed, and after making the corrections, it passed. So it's not just a happy pass, it actually catches the bug.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/query-core/src/__tests__/mutationObserver.test.tsx (1)

59-73: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the resubscribe state notification.

The test checks only the final success callback. It does not verify that resubscribe immediately emits the current pending result or that the final result clears isPending. Add both assertions to cover the onSubscribe() refresh and notification behavior.

Proposed test assertions
 mutation.subscribe(subscriptionHandler)
+ expect(subscriptionHandler).toHaveBeenLastCalledWith(+ expect.objectContaining({+ status: 'pending',+ isPending: true,+ }),+ )
await vi.advanceTimersByTimeAsync(20)
expect.objectContaining({
status: 'success',
data: 'input',
+ isPending: false,
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/query-core/src/__tests__/mutationObserver.test.tsx` around lines 59
- 73, Update the mutationObserver test around mutation.subscribe and
subscriptionHandler to assert that resubscribing immediately notifies the
handler with the current pending result, including isPending: true, before
advancing timers. After the mutation completes, retain the success assertions
and additionally verify the final result has isPending: false.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/query-core/src/__tests__/mutationObserver.test.tsx`:
- Around line 59-73: Update the mutationObserver test around mutation.subscribe
and subscriptionHandler to assert that resubscribing immediately notifies the
handler with the current pending result, including isPending: true, before
advancing timers. After the mutation completes, retain the success assertions
and additionally verify the final result has isPending: false.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7b7b6516-4255-43ae-a1dc-124749767e7d

📥 Commits

Reviewing files that changed from the base of the PR and between ac3136a and b849bce.

📒 Files selected for processing (3)
  • .changeset/mutation-observer-resubscribe.md
  • packages/query-core/src/__tests__/mutationObserver.test.tsx
  • packages/query-core/src/mutationObserver.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/query-core/src/mutationObserver.ts

@coderabbitai

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@nx-cloud

nx-cloudBot commented Aug 20, 2026

Copy link
Copy Markdown

🤖 Nx Cloud AI Fix Eligible

An automatically generated fix could have helped fix failing tasks for this run, but Self-healing CI is disabled for this workspace. Visit workspace settings to enable it and get automatic fixes in future runs.

To disable these notifications, a workspace admin can disable them in workspace settings.


View your CI Pipeline Execution ↗ for commit 3cee7e6

CommandStatusDurationResult
nx affected --targets=test:sherif,test:knip,tes...❌ Failed3m 43sView ↗
nx run-many --target=build --exclude=examples/*...❌ Failed3sView ↗

☁️ Nx Cloud last updated this comment at 2026-08-20 14:56:58 UTC

@TkDodo

Copy link
Copy Markdown
Collaborator

@TkDodoTkDodo closed this Aug 20, 2026
Sign up for freeto 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.

3 participants

@leonardoag2026-beep@TkDodo@MILLERMARRU