Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions packages/react-query/src/__tests__/useMutationState.test-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,42 @@ describe('useMutationState', () => {

expectTypeOf(result).toEqualTypeOf<Array<MutationStatus>>()
})
it('should propagate mutation generics to select callback', () => {
type MyData = { id: number }
type MyError = { code: string }
type MyVariables = { name: string }

const result = useMutationState<MyData, MyError, MyVariables>({
filters: { mutationKey: ['my-mutation'] },
select: (mutation) => {
// mutation should be typed as Mutation<MyData, MyError, MyVariables, unknown>
expectTypeOf(mutation.state.data).toEqualTypeOf<MyData | undefined>()
expectTypeOf(mutation.state.error).toEqualTypeOf<MyError | null>()
expectTypeOf(mutation.state.variables).toEqualTypeOf<
MyVariables | undefined
>()
return mutation.state
},
})

expectTypeOf(result).toEqualTypeOf<
Array<MutationState<MyData, MyError, MyVariables, unknown>>
>()
})
it('should allow custom TResult when providing select', () => {
type MyVariables = { userId: string }

const result = useMutationState<
unknown,
Error,
MyVariables,
unknown,
string
>({
filters: { status: 'pending' },
select: (mutation) => mutation.state.variables?.userId ?? '',
})

expectTypeOf(result).toEqualTypeOf<Array<string>>()
})
})
45 changes: 38 additions & 7 deletions packages/react-query/src/useMutationState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as React from 'react'
import { notifyManager, replaceEqualDeep } from '@tanstack/query-core'
import { useQueryClient } from './QueryClientProvider'
import type {
DefaultError,
Mutation,
MutationCache,
MutationFilters,
Expand All @@ -22,25 +23,55 @@ export function useIsMutating(
).length
}

type MutationStateOptions<TResult = MutationState> = {
type MutationStateOptions<
TData = unknown,
TError = DefaultError,
TVariables = unknown,
TContext = unknown,
TResult = MutationState<TData, TError, TVariables, TContext>,
> = {
filters?: MutationFilters
select?: (mutation: Mutation) => TResult
select?: (
mutation: Mutation<TData, TError, TVariables, TContext>,
) => TResult
}

function getResult<TResult = MutationState>(
function getResult<
TData = unknown,
TError = DefaultError,
TVariables = unknown,
TContext = unknown,
TResult = MutationState<TData, TError, TVariables, TContext>,
>(
mutationCache: MutationCache,
options: MutationStateOptions<TResult>,
options: MutationStateOptions<TData, TError, TVariables, TContext, TResult>,
): Array<TResult> {
return mutationCache
.findAll(options.filters)
.map(
(mutation): TResult =>
(options.select ? options.select(mutation) : mutation.state) as TResult,
(options.select
? options.select(
mutation as unknown as Mutation<TData, TError, TVariables, TContext>,
)
: mutation.state) as TResult,
)
}

export function useMutationState<TResult = MutationState>(
options: MutationStateOptions<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
> = {},
Comment on lines +61 to +74
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.

queryClient?: QueryClient,
): Array<TResult> {
const mutationCache = useQueryClient(queryClient).getMutationCache()
Expand Down
Loading