fix(react-query): propagate Mutation generics in useMutationState#10275
fix(react-query): propagate Mutation generics in useMutationState#10275mixelburg wants to merge 1 commit intoTanStack:mainfrom
Conversation
|
📝 WalkthroughWalkthroughThis change enhances the Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
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. Comment Tip You can disable the changed files summary in the walkthrough.Disable the |
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
packages/react-query/src/__tests__/useMutationState.test-d.tsxpackages/react-query/src/useMutationState.ts
| export function useMutationState< | ||
| TData = unknown, | ||
| TError = DefaultError, | ||
| TVariables = unknown, | ||
| TContext = unknown, | ||
| TResult = MutationState<TData, TError, TVariables, TContext>, | ||
| >( | ||
| options: MutationStateOptions< | ||
| TData, | ||
| TError, | ||
| TVariables, | ||
| TContext, | ||
| TResult | ||
| > = {}, |
There was a problem hiding this comment.
🧩 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 -100Repository: TanStack/query
Length of output: 3329
🏁 Script executed:
fd -e 'ts' -e 'tsx' | xargs grep -l 'useMutationState.*<.*>' | head -20Repository: TanStack/query
Length of output: 307
🏁 Script executed:
cat -n packages/react-query/src/__tests__/useMutationState.test-d.tsxRepository: 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-dotallRepository: TanStack/query
Length of output: 2004
🏁 Script executed:
cat -n packages/react-query/src/useMutationState.ts | tail -50Repository: 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 -B2Repository: 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.
travisbreaks
left a comment
There was a problem hiding this comment.
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
TResultstays in the first position and the mutation generics come from a nested type (e.g., viaMutationFilterscarrying generics, which would also enable inference frommutationKey).
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
Mutationtype passed toselect. 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 +
selectcase still infersTResultcorrectly (the existing "should infer with select" test covers this, but confirming it still passes with the new signature is important). - A test with
TContextexplicitly set (all current tests leave it atunknown).
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.
Closes #9825
Problem
useMutationState'sselectcallback always received aMutation<unknown, Error, unknown, unknown>regardless of any type annotations, making it impossible to write type-safeselectfunctions without manual casts.Before —
mutationinselectis fully untyped:Fix
Added four generic type parameters —
TData,TError,TVariables,TContext— to bothMutationStateOptionsanduseMutationState, following the same parameter order used byuseMutation. These default to the same types as before (unknown/DefaultError/unknown/unknown), so all existing code compiles without changes.After — mutation is fully typed:
A common pattern that now works without casts:
Changes
packages/react-query/src/useMutationState.ts: AddedTData,TError,TVariables,TContextgenerics toMutationStateOptions,getResult, anduseMutationState.packages/react-query/src/__tests__/useMutationState.test-d.tsx: Added two new type tests that verify generic propagation and customTResultsupport.Backward compatibility
All existing callers continue to compile unchanged — the new generics default to exactly what the old unparameterized
Mutationresolved to.Summary by CodeRabbit
Release Notes
Tests
Chores