Skip to content

fix(react-query): propagate Mutation generics in useMutationState#10275

Open
mixelburg wants to merge 1 commit intoTanStack:mainfrom
mixelburg:fix/useMutationState-generics
Open

fix(react-query): propagate Mutation generics in useMutationState#10275
mixelburg wants to merge 1 commit intoTanStack:mainfrom
mixelburg:fix/useMutationState-generics

Conversation

@mixelburg
Copy link

@mixelburg mixelburg commented Mar 16, 2026

Closes #9825

Problem

useMutationState's select callback always received a Mutation<unknown, Error, unknown, unknown> regardless of any type annotations, making it impossible to write type-safe select functions without manual casts.

Beforemutation in select is fully untyped:

// ❌ mutation is Mutation<unknown, Error, unknown, unknown>; state.data is unknown
const result = useMutationState({
  filters: { mutationKey: ['my-mutation'] },
  select: (mutation) => mutation.state.variables, // variables: unknown
})

Fix

Added four generic type parameters — TData, TError, TVariables, TContext — to both MutationStateOptions and useMutationState, following the same parameter order used by useMutation. These default to the same types as before (unknown/DefaultError/unknown/unknown), so all existing code compiles without changes.

After — mutation is fully typed:

// ✅ mutation is Mutation<MyData, MyError, MyVariables, unknown>
const result = useMutationState<MyData, MyError, MyVariables>({
  filters: { mutationKey: ['my-mutation'] },
  select: (mutation) => mutation.state.variables, // variables: MyVariables | undefined
})

A common pattern that now works without casts:

type Vars = { userId: string }
const pendingVars = useMutationState<unknown, Error, Vars>({
  filters: { status: 'pending' },
  select: (m) => m.state.variables,
}) // Array<Vars | undefined>

Changes

  • packages/react-query/src/useMutationState.ts: Added TData, TError, TVariables, TContext generics to MutationStateOptions, getResult, and useMutationState.
  • packages/react-query/src/__tests__/useMutationState.test-d.tsx: Added two new type tests that verify generic propagation and custom TResult support.

Backward compatibility

All existing callers continue to compile unchanged — the new generics default to exactly what the old unparameterized Mutation resolved to.

Summary by CodeRabbit

Release Notes

  • Tests

    • Added comprehensive type-focused tests for mutation state utilities, covering generic parameter propagation and custom result type inference.
  • Chores

    • Enhanced generic type support for mutation state operations, enabling more flexible type inference and custom return types while maintaining existing runtime behavior.

@changeset-bot
Copy link

changeset-bot bot commented Mar 16, 2026

⚠️ No Changeset found

Latest commit: 06c2fc6

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 16, 2026

📝 Walkthrough

Walkthrough

This change enhances the useMutationState hook and related utilities with full generic type propagation. The MutationStateOptions interface and both getResult and useMutationState functions now accept generic parameters for data, error, variables, and context types, enabling proper type inference in select callbacks. Tests are added to verify the generic typing behavior.

Changes

Cohort / File(s) Summary
Type definition and implementation enhancements
packages/react-query/src/useMutationState.ts
Enhanced MutationStateOptions, getResult, and useMutationState to accept full generic parameters (TData, TError, TVariables, TContext, TResult) with appropriate defaults. Added DefaultError import and cast mutations to generic type within select function for proper type safety.
Type testing additions
packages/react-query/src/__tests__/useMutationState.test-d.tsx
Added two new type-focused tests verifying generic parameter propagation to select callbacks and custom TResult typing inference.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Generics now flow through mutations with grace,
Type safety prevails in each select and case,
No more casting needed, the types align true,
From data to errors, the path flows right through!
React Query hops forward with strength in its stride!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding generic type parameter propagation to useMutationState.
Description check ✅ Passed The description provides a clear problem statement, before/after examples, implementation details, and confirms backward compatibility; matches template structure with complete changes section.
Linked Issues check ✅ Passed The PR fully addresses issue #9825 by adding TData, TError, TVariables, TContext generics to useMutationState, enabling type-safe select callbacks that match the expected behavior.
Out of Scope Changes check ✅ Passed All changes are directly related to the linked issue: generic parameter additions to useMutationState and supporting type tests; no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

Tip

You can disable the changed files summary in the walkthrough.

Disable the reviews.changed_files_summary setting to disable the changed files summary in the walkthrough.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/react-query/src/useMutationState.ts`:
- Around line 61-74: The public generic ordering of useMutationState was changed
and breaks calls that previously used a single generic to specify TResult (e.g.
useMutationState<string>({...})); restore backward compatibility by adding a
one-generic overload for useMutationState<TResult = MutationState<any,
DefaultError, any, any, TResult>>(options?: MutationStateOptions<any,
DefaultError, any, any, TResult>): TResult (or equivalent overload signature)
above the existing multi-generic declaration so single-generic calls bind to
TResult as before, update the implementation signature to remain the
multi-generic form, and add a type-level test that asserts
useMutationState<string>({ select: ... }) infers string as the TResult to
prevent regressions; reference the function name useMutationState and the type
MutationState / MutationStateOptions in the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a8df3163-53e2-422c-ac84-3d13659dc201

📥 Commits

Reviewing files that changed from the base of the PR and between d4fd941 and 06c2fc6.

📒 Files selected for processing (2)
  • packages/react-query/src/__tests__/useMutationState.test-d.tsx
  • packages/react-query/src/useMutationState.ts

Comment on lines +61 to +74
export function useMutationState<
TData = unknown,
TError = DefaultError,
TVariables = unknown,
TContext = unknown,
TResult = MutationState<TData, TError, TVariables, TContext>,
>(
options: MutationStateOptions<
TData,
TError,
TVariables,
TContext,
TResult
> = {},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP --type=ts --type=tsx -C2 '\buseMutationState\s*<\s*[^,>]+\s*>\s*\('

Repository: TanStack/query

Length of output: 85


🏁 Script executed:

#!/bin/bash
# Fix: use ts extension pattern instead of tsx type flag
rg -nP --type=ts -C2 'useMutationState\s*<\s*[^,>]+\s*>\s*\(' --include='*.tsx'

Repository: TanStack/query

Length of output: 499


🏁 Script executed:

cat -n packages/react-query/src/useMutationState.ts | head -100

Repository: TanStack/query

Length of output: 3329


🏁 Script executed:

fd -e 'ts' -e 'tsx' | xargs grep -l 'useMutationState.*<.*>' | head -20

Repository: TanStack/query

Length of output: 307


🏁 Script executed:

cat -n packages/react-query/src/__tests__/useMutationState.test-d.tsx

Repository: TanStack/query

Length of output: 2439


🏁 Script executed:

rg 'useMutationState\s*<' -A3 packages/react-query/src/__tests__/

Repository: TanStack/query

Length of output: 882


🏁 Script executed:

rg 'useMutationState\s*<\s*[^,>]+\s*>\s*\(' -A2 --multiline-dotall

Repository: TanStack/query

Length of output: 2004


🏁 Script executed:

cat -n packages/react-query/src/useMutationState.ts | tail -50

Repository: TanStack/query

Length of output: 1648


🏁 Script executed:

cat -n docs/framework/react/guides/optimistic-updates.md | grep -A5 -B5 'useMutationState<string>'

Repository: TanStack/query

Length of output: 441


🏁 Script executed:

rg 'useMutationState' -i packages/react-query/CHANGELOG.md -A2 -B2

Repository: TanStack/query

Length of output: 40


Preserve the legacy useMutationState<TResult> call shape with a backward-compatible overload.

This reorders the public generics in a source-breaking way. The documentation at docs/framework/react/guides/optimistic-updates.md shows a pattern where useMutationState<string>({ select: ... }) uses the generic to specify a custom TResult type. With the new signature, string binds to TData instead, breaking this documented example. Other framework implementations (preact, vue, svelte, solid) maintain a single-generic signature for backward compatibility.

Add a one-generic overload that preserves the TResult binding for existing call sites, and include a type test covering this pattern to prevent regressions.

🧩 Backward-compatible overload sketch
+export function useMutationState<TResult = MutationState>(
+  options?: MutationStateOptions<
+    unknown,
+    DefaultError,
+    unknown,
+    unknown,
+    TResult
+  >,
+  queryClient?: QueryClient,
+): Array<TResult>
 export function useMutationState<
   TData = unknown,
   TError = DefaultError,
   TVariables = unknown,
   TContext = unknown,
   TResult = MutationState<TData, TError, TVariables, TContext>,
 >(
   options: MutationStateOptions<
     TData,
     TError,
     TVariables,
     TContext,
     TResult
   > = {},
   queryClient?: QueryClient,
 ): Array<TResult> {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-query/src/useMutationState.ts` around lines 61 - 74, The
public generic ordering of useMutationState was changed and breaks calls that
previously used a single generic to specify TResult (e.g.
useMutationState<string>({...})); restore backward compatibility by adding a
one-generic overload for useMutationState<TResult = MutationState<any,
DefaultError, any, any, TResult>>(options?: MutationStateOptions<any,
DefaultError, any, any, TResult>): TResult (or equivalent overload signature)
above the existing multi-generic declaration so single-generic calls bind to
TResult as before, update the implementation signature to remain the
multi-generic form, and add a type-level test that asserts
useMutationState<string>({ select: ... }) infers string as the TResult to
prevent regressions; reference the function name useMutationState and the type
MutationState / MutationStateOptions in the change.

Copy link

@travisbreaks travisbreaks left a comment

Choose a reason for hiding this comment

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

Review: Generic propagation for useMutationState

Thanks for tackling this. The core idea (propagating mutation generics into the select callback) is the right fix for #9825. A few observations on the implementation:

1. Breaking change to generic parameter positions (main concern)

The old signature was:

useMutationState<TResult = MutationState>(options, queryClient)

The new signature is:

useMutationState<TData, TError, TVariables, TContext, TResult>(options, queryClient)

This means any existing code using a single explicit type parameter:

useMutationState<MyCustomState>({ select: ... })

...silently changes meaning. MyCustomState was binding to TResult before; now it binds to TData. TypeScript does not support partial type argument inference, so all positional generics shift.

This is a source-level breaking change for anyone who was passing an explicit TResult generic. The fix would be one of:

  • Function overloads: a single-generic overload useMutationState<TResult>(...) for backward compat, plus the new multi-generic overload.
  • Wrapper type / options object: restructure so TResult stays in the first position and the mutation generics come from a nested type (e.g., via MutationFilters carrying generics, which would also enable inference from mutationKey).

The overload approach is probably the lightest path. CodeRabbit flagged the same thing; I agree it needs addressing before merge.

2. The as unknown as Mutation<...> cast is inherently unsound (but acceptable)

mutationCache.findAll() returns Array<Mutation> (effectively Mutation<any, any, any, any>). The cast:

mutation as unknown as Mutation<TData, TError, TVariables, TContext>

...is a trust-the-developer assertion. If filters match mutations with different generic shapes, the types lie at runtime. This is the same trade-off useQueryState makes, and it's fine as long as the docs are clear that the generics are a narrowing assertion, not a guarantee. Worth a JSDoc note on the function or the select property, something like:

"The generic parameters narrow the Mutation type passed to select. They are not validated at runtime; ensure your filters match mutations of the expected shape."

3. Type test coverage

The two new type tests cover the happy path well. Consider adding:

  • A test that verifies the zero-explicit-generics + select case still infers TResult correctly (the existing "should infer with select" test covers this, but confirming it still passes with the new signature is important).
  • A test with TContext explicitly set (all current tests leave it at unknown).

4. Missing changeset

The changeset bot flagged this already. Since this changes the public API's generic signature, it should be at least a patch (arguably minor since it adds new type-level capability, though the positional shift could be seen as a breaking change depending on the project's semver policy for types).

Summary

The type propagation logic itself is correct. The blocking issue is the generic parameter ordering breaking existing single-generic callers. An overload would resolve that cleanly. Everything else is minor.

Good work overall. The before/after in the PR description is clear and the type tests are well-structured.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useMutationState does not propagate generics into select callback (type inference lost)

2 participants