Skip to content

feat(react-query): add mutationOptions #8960

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
596896d
feat(react-query): add mutationOptions
Ubinquitous Apr 6, 2025
ff15e5d
test(react-query): add DataTag test case
Ubinquitous Apr 7, 2025
ea54b58
Merge branch 'main' into feature/react-query-mutation-options
TkDodo May 1, 2025
2972edd
fix(react-query): remove unnecessary types from mutation
Ubinquitous May 1, 2025
08a5026
fix(react-query): remove unncessary type overload
Ubinquitous May 1, 2025
f3b74c0
Merge branch 'main' into feature/react-query-mutation-options
manudeli May 1, 2025
a4560d3
Merge branch 'main' into feature/react-query-mutation-options
manudeli May 5, 2025
6889638
chore(react-query): add mutationOptions to barrel file
Ubinquitous May 5, 2025
b844dee
Merge branch 'main' into feature/react-query-mutation-options
manudeli May 6, 2025
e61227d
Merge branch 'main' into feature/react-query-mutation-options
manudeli May 6, 2025
33d3e9f
fix(react-query): fix test eslint issue
Ubinquitous May 7, 2025
fd7b9f9
docs(react-query): add more examples
Ubinquitous May 7, 2025
6ee8c76
Merge branch 'main' into feature/react-query-mutation-options
manudeli May 7, 2025
299a19f
Merge branch 'main' into feature/react-query-mutation-options
manudeli May 9, 2025
2622902
Merge branch 'main' into feature/react-query-mutation-options
TkDodo May 13, 2025
9ded37d
test(react-query): add more test cases
Ubinquitous May 20, 2025
48d867b
chore(react-query): Change mutaitonKey to required
Ubinquitous Jun 6, 2025
05f4fc0
fix(react-query): fix test code type error
Ubinquitous Jun 6, 2025
b202d6e
test(react-query): add testcase when used with other mutation util
Ubinquitous Jun 7, 2025
167fb8c
fix(react-query): fix error test code and avoid use deprecateed method
Ubinquitous Jun 7, 2025
2b85c72
fix(react-query): fix error test code and avoid use deprecateed method
Ubinquitous Jun 7, 2025
df3545a
fix(react-query): fix import detect error
Ubinquitous Jun 7, 2025
08769ac
fix(react-query): fix import detect error
Ubinquitous Jun 7, 2025
36a8af1
fix(react-query): add function overload
Ubinquitous Jun 10, 2025
22f5ed2
test(react-query): fix mutation options test code
Ubinquitous Jun 10, 2025
1661565
Update docs/framework/react/typescript.md
TkDodo Jun 21, 2025
d7587ba
Merge branch 'main' into feature/react-query-mutation-options
TkDodo Jun 21, 2025
7ee1f5a
Update docs/framework/react/reference/mutationOptions.md
TkDodo Jun 21, 2025
d682a88
Update docs/framework/react/typescript.md
manudeli Jun 21, 2025
f32a7e0
fix: update mutationOptions type definition to allow optional mutatio…
manudeli Jun 22, 2025
0729167
ci: apply automated fixes
autofix-ci[bot] Jun 22, 2025
8730872
Merge branch 'main' into feature/react-query-mutation-options
manudeli Jun 22, 2025
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
15 changes: 15 additions & 0 deletions docs/framework/react/reference/mutationOptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
id: mutationOptions
title: mutationOptions
---

```tsx
mutationOptions({
mutationFn,
...options,
})
```

**Options**

You can generally pass everything to `mutationOptions` that you can also pass to [`useMutation`](./useMutation.md).
20 changes: 20 additions & 0 deletions docs/framework/react/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,26 @@ const data = queryClient.getQueryData<Group[]>(['groups'])
[//]: # 'TypingQueryOptions'
[//]: # 'Materials'

## Typing Mutation Options

Similarly to `queryOptions`, you can use `mutationOptions` to extract mutation options into a separate function:

```ts
function groupMutationOptions() {
return mutationOptions({
mutationKey: ['groups'],
mutationFn: addGroup,
})
}

useMutation({
...groupMutationOptions()
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['groups'] })
})
useIsMutating(groupMutationOptions())
queryClient.isMutating(groupMutationOptions())
```

## Further Reading

For tips and tricks around type inference, have a look at [React Query and TypeScript](./community/tkdodos-blog.md#6-react-query-and-typescript) from
Expand Down
26 changes: 26 additions & 0 deletions packages/react-query/src/__tests__/mutationOptions.test-d.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expectTypeOf, it } from 'vitest'
import { mutationOptions } from '../mutationOptions'

describe('mutationOptions', () => {
it('should not allow excess properties', () => {
return mutationOptions({
mutationFn: () => Promise.resolve(5),
mutationKey: ['key'],
// @ts-expect-error this is a good error, because onMutates does not exist!
onMutates: 1000,
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
})
})

it('should infer types for callbacks', () => {
return mutationOptions({
mutationFn: () => Promise.resolve(5),
mutationKey: ['key'],
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
})
})
})
Copy link
Collaborator

Choose a reason for hiding this comment

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

In my opinion, we should add more test cases before merging this Pull Request—assuming TkDodo agrees with the proposed interface. I'm not sure whether TkDodo will agree with this interface.

const Example = () => {
  const mutation = useMutation({
    ...mutationOptions({
      mutationKey: ['key'],
      mutationFn: () => Promise.resolve({ field: 'test' })
    }),
    onSuccess: (data) => expectTypeOf(data).toEqualTypeOf<{ field: string }>()
  })
  expectTypeOf(mutation).toEqualTypeOf ...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, I added more test cases along with the ones that work with useMutation.

14 changes: 14 additions & 0 deletions packages/react-query/src/__tests__/mutationOptions.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, expect, it } from 'vitest'
import { mutationOptions } from '../mutationOptions'
import type { UseMutationOptions } from '../types'

describe('mutationOptions', () => {
it('should return the object received as a parameter without any modification.', () => {
const object: UseMutationOptions = {
mutationKey: ['key'],
mutationFn: () => Promise.resolve(5),
} as const

expect(mutationOptions(object)).toStrictEqual(object)
})
})
1 change: 1 addition & 0 deletions packages/react-query/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,6 @@ export {
export { useIsFetching } from './useIsFetching'
export { useIsMutating, useMutationState } from './useMutationState'
export { useMutation } from './useMutation'
export { mutationOptions } from './mutationOptions'
export { useInfiniteQuery } from './useInfiniteQuery'
export { useIsRestoring, IsRestoringProvider } from './IsRestoringProvider'
13 changes: 13 additions & 0 deletions packages/react-query/src/mutationOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { DefaultError } from '@tanstack/query-core'
import type { UseMutationOptions } from './types'

export function mutationOptions<
TData = unknown,
TError = DefaultError,
TVariables = void,
TContext = unknown,
>(
options: UseMutationOptions<TData, TError, TVariables, TContext>,
): UseMutationOptions<TData, TError, TVariables, TContext> {
return options
}
Copy link
Collaborator

@manudeli manudeli May 5, 2025

Choose a reason for hiding this comment

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

If mutationOptions is intended to be reused across various TanStack React Query interfaces—such as useMutation, useIsMutating, and queryClient.isMutating—then it might make sense to make mutationKey a required field, similar to how queryOptions.queryKey is typed.

Suggested change
import type { DefaultError } from '@tanstack/query-core'
import type { UseMutationOptions } from './types'
export function mutationOptions<
TData = unknown,
TError = DefaultError,
TVariables = void,
TContext = unknown,
>(
options: UseMutationOptions<TData, TError, TVariables, TContext>,
): UseMutationOptions<TData, TError, TVariables, TContext> {
return options
}
import type { WithRequired } from 'node_modules/@tanstack/query-core/build/legacy'
import type { DefaultError } from '@tanstack/query-core'
import type { UseMutationOptions } from './types'
export function mutationOptions<
TData = unknown,
TError = DefaultError,
TVariables = void,
TContext = unknown,
>(
options: WithRequired<
UseMutationOptions<TData, TError, TVariables, TContext>,
'mutationKey'
>,
): WithRequired<
UseMutationOptions<TData, TError, TVariables, TContext>,
'mutationKey'
> {
return options
}

Making mutationKey required could help avoid situations like the following:

// Without mutationKey, it’s unavailable for useIsMutating or queryClient.isMutating
// cannot reliably identify the mutation, which may lead to unintended behavior.
function groupMutationOptions() {
  return mutationOptions({
    mutationFn: addGroup,
  });
}

useMutation({
  ...groupMutationOptions(),
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['groups'] }),
});

useIsMutating(groupMutationOptions())
// This cannot detect the isMutating state from the above hook 
// because groupMutationOptions doesn't include a mutationKey.
// but TypeScript compiler doesn't detect this as error

So in my opinion, mutationOptions's mutationKey should be required field.
Additionally, we can make it as optional field later if we need without BREAKING CHANGE.

So when we first add mutationOptions, it might be beneficial to make mutationKey a required field at first

Copy link
Contributor Author

@Ubinquitous Ubinquitous May 5, 2025

Choose a reason for hiding this comment

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

If mutationOptions is used not only in useMutation but also in other interfaces such as useIsMutating, I think it would be better to make it a required value.

One thing I'm concerned about is that developers who only use mutationOptions for useMutation's options might end up writing unnecessary code.

Copy link
Collaborator

Choose a reason for hiding this comment

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

yeah we can’t make it required; yes filters won’t work then; it’s one of the reasons why I’m against this helper in the first place 😅

Copy link
Contributor

Choose a reason for hiding this comment

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

Can't we make mutationKey required? I know its not really used in useMutation often, but it forces us to write code that would work with any api.
I can think of developers wondering why useIsMutating isn't working when they forgot the key.

Or if we can't do this add a default key the api can fall back to if no key is provided?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Tbh I don’t know what the use-case for this helper will be. It’s why I’m so hesitant to add it. I tried to look through issues / discussion to find what people want this for, and the most things I found was:

to do this same sharing/separation for mutations
This will make it easier to create custom hooks.

I'd like this for useMutationState
we want to show a state for mutation in progress on a certain item

I think the first 2 usages would be quite surprised that mutationKey is required, while for the last 2, they would be quite surprised if filter didn’t work as expected. It’s also worth noticing that filters can work without a key - the key is not required in filters.

Since it’s easier to go from required -> optional, I think it’s better to make it required, then see the feedback and loosen it up to optional if there’s lots of negative feedback. @manudeli FYI

Copy link
Collaborator

Choose a reason for hiding this comment

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

I also feel that this is a somewhat controversial interface, so I'm unsure whether it's the right decision to add it immediately. I agree that if we do decide to add mutationOptions, it makes sense to first add mutationOptions with mutationKey as a required field, and then potentially make it optional later based on feedback.

Also, I think it would be good to mark mutationOptions as experimental in the JSDoc

Copy link
Collaborator

Choose a reason for hiding this comment

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

Since it’s easier to go from required -> optional, I think it’s better to make it required, then see the feedback and loosen it up to optional if there’s lots of negative feedback. @manudeli FYI

@TkDodo If we need to provide mutationKey as optional at that time, what if we allowed developers to opt into making it optional by registering it through the Register interface like defaultError? If so, it seems we could offer mutationOptions tailored to library users.

import '@tanstack/react-query'

declare module '@tanstack/react-query' {
  interface Register {
    // This is a tentative name
    mutationOptionsMutationKey: 'optional' // default is 'required'
  }
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

that’s an interesting approach. I think we should try to make it required right now and see how it’s received. if there’s pushback we can either make it optional (non-breaking) or add the module augmentation override.

But maybe required is fine for most people 👍

Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed mutationKey to required along with updated test code and documentation.

Loading