Skip to content

fix(query-core): unpause a mutation that can start after its onMutate - #11788

Open
maxymlyskov wants to merge 1 commit into
TanStack:mainfrom
maxymlyskov:fix/unpause-mutation-after-onmutate
Open

maxymlyskov wants to merge 1 commit into
TanStack:mainfrom
maxymlyskov:fix/unpause-mutation-after-onmutate

Conversation

@maxymlyskov

@maxymlyskov maxymlyskov commented Sep 30, 2026 •

Copy link
Copy Markdown

🎯 Changes

A mutation that can't start when mutate() is called (queued behind another mutation in the same scope, or offline) starts as pending with isPaused: true. If it becomes able to start while its onMutate is still running, retryer.start() runs the mutationFn straight away, but nothing dispatches continue, so isPaused stays true for the whole request.

defaultShouldDehydrateMutation reads that flag, so the in-flight mutation gets dehydrated, and after a restore resumePausedMutations() sends it again. Dehydrating while the second of two scoped mutations is in flight, then hydrating into a fresh client and resuming, main sends a, b, b and this branch sends a, b. That is the case #6238 calls unwanted: an in-flight mutation shouldn't be persisted and replayed.

The fix dispatches continue right before retryer.start() when the mutation is still marked paused but can start now, the same dispatch #9015 added for restored mutations. It reads this.state.isPaused, so a restored mutation that was already continued doesn't get a second one.

Tests in mutation.test.tsx:

  • a mutation that goes back online while its onMutate runs is unpaused once it starts. On main it stays isPaused: true.
  • a queued mutation whose onMutate resolves before its turn stays paused. It passes on main and fails if the fix unpauses too early.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested code changes locally with pnpm run test:pr, or these tests do not apply to this pull request.
  • I have followed the AI contribution policy and fully understand the code in this pull request, including any code generated with AI assistance.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Bug Fixes
    • Mutations that become eligible to run while onMutate is still resolving now resume correctly. This covers mutations waiting for connectivity and preserves the paused state of mutations queued behind another mutation.

Mutation.execute() computes isPaused before it awaits the MutationCache and
mutation onMutate callbacks, and dispatches pending with that value. When the
mutation cannot start at that point (queued behind another mutation in its
scope, or offline) but can by the time onMutate resolves, retryer.start()
runs it directly instead of going through pause(), so no continue action is
dispatched and the mutation reports isPaused: true for its whole request.
defaultShouldDehydrateMutation and resumePausedMutations read that flag, so
the in-flight mutation is dehydrated as not yet sent and runs a second time
after a restore.

Dispatch continue right before start() when the mutation is still marked
paused but can start now, as TanStack#9015 does for a restored mutation.

Source: packages/query-core/src/mutation.ts execute(),
packages/query-core/src/retryer.ts start(), hydration.ts
defaultShouldDehydrateMutation.
@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: TanStack/query/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 19d3fb01-6ce0-40b8-a1ef-d4fa1239e597

📥 Commits

Reviewing files that changed from the base of the PR and between 5d14b15 and 934a88c.

📒 Files selected for processing (3)
  • .changeset/unpause-mutation-after-onmutate.md
  • packages/query-core/src/__tests__/mutation.test.tsx
  • packages/query-core/src/mutation.ts

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


📝 Walkthrough

Walkthrough

After onMutate resolves, Mutation.execute now dispatches continue when the mutation is still paused and its retryer can start. Tests cover scoped and offline mutations.

Changes

Mutation pause state

Layer / File(s) Summary
Unpause mutations and verify pause cases
packages/query-core/src/mutation.ts, packages/query-core/src/__tests__/mutation.test.tsx, .changeset/unpause-mutation-after-onmutate.md
Mutation.execute dispatches continue when the mutation is paused and its retryer can start after onMutate. Tests cover a scoped mutation that remains paused and an offline mutation that resumes and succeeds. The changeset records a patch for @tanstack/query-core.

Priority: ➖ Normal

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

Change: Bug fix

Suggested reviewers: sukvvon

Merge Risk: ⚪ Minimal · up to 934a8

The change clears stale pause state when a mutation becomes eligible to run, while preserving required queue pauses. No actionable merge-blocking risk remains based on the supplied evidence.

Security Architecture Review

Security architecture risk: ⚪ Minimal · up to 934a8

The fix aligns pause state with execution eligibility, reducing unintended mutation replay without bypassing scoped ordering or network-mode controls. No material security risk was identified in the reviewed transition.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The directly affected scope is consumer mutations that become eligible while onMutate is pending, including those selected for default dehydration. Actual backend side effects and tenant or asset exposure depend on each application's mutationFn and are not established by the supplied library evidence.

Trust Boundaries and Controls

  • inferred — The new notification does not independently authorize mutation execution: existing start eligibility and scoped ownership checks remain in the execution path. The inspected change therefore does not expand caller-controlled reachability to mutationFn or introduce a new credential or identity transition.

Resilience and Maintainability Implications

  • observed — Recovery and failure containment retain their existing mechanisms. Resume reuses an existing retryer, restored execution already clears pause state, terminal reducers clear the flag, and execute's finally path releases its retryer reference and advances the scoped queue. The fix addresses stale persistence eligibility without establishing a general exactly-once side-effect guarantee.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: unpausing a query-core mutation after its onMutate callback allows it to start.
Description check ✅ Passed The description explains the bug, its impact on dehydration and restore behavior, the fix, test coverage, and release impact. It also includes the required template sections and completed checklist it…
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 2…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

This branch has not been deployed

No deployments
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.

1 participant