query icon indicating copy to clipboard operation
query copied to clipboard

feat(react-query): add mutationOptions

Open Ubinquitous opened this issue 7 months ago • 15 comments

mutationOptions helps extracting mutation options into separate functions.

Ubinquitous avatar Apr 06 '25 10:04 Ubinquitous

View your CI Pipeline Execution ↗ for commit 357269fdbc15029a1badefe555d201b92cee7315

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 46s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 2s View ↗

☁️ Nx Cloud last updated this comment at 2025-07-09 14:13:27 UTC

nx-cloud[bot] avatar Apr 06 '25 10:04 nx-cloud[bot]

More templates

@tanstack/angular-query-devtools-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-devtools-experimental@8960
@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@8960
@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@8960
@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@8960
@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@8960
@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@8960
@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@8960
@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@8960
@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@8960
@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@8960
@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@8960
@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@8960
@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@8960
@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@8960
@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@8960
@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@8960
@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@8960
@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@8960
@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@8960
@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@8960
@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@8960

commit: 357269f

pkg-pr-new[bot] avatar Apr 06 '25 10:04 pkg-pr-new[bot]

Codecov Report

All modified and coverable lines are covered by tests :white_check_mark:

Project coverage is 84.38%. Comparing base (ed8cc23) to head (357269f). Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             main    #8960       +/-   ##
===========================================
+ Coverage   45.37%   84.38%   +39.00%     
===========================================
  Files         207       26      -181     
  Lines        8277      365     -7912     
  Branches     1865      107     -1758     
===========================================
- Hits         3756      308     -3448     
+ Misses       4080       48     -4032     
+ Partials      441        9      -432     
Components Coverage Δ
@tanstack/angular-query-devtools-experimental ∅ <ø> (∅)
@tanstack/angular-query-experimental ∅ <ø> (∅)
@tanstack/eslint-plugin-query ∅ <ø> (∅)
@tanstack/query-async-storage-persister ∅ <ø> (∅)
@tanstack/query-broadcast-client-experimental ∅ <ø> (∅)
@tanstack/query-codemods ∅ <ø> (∅)
@tanstack/query-core ∅ <ø> (∅)
@tanstack/query-devtools ∅ <ø> (∅)
@tanstack/query-persist-client-core ∅ <ø> (∅)
@tanstack/query-sync-storage-persister ∅ <ø> (∅)
@tanstack/query-test-utils ∅ <ø> (∅)
@tanstack/react-query 95.95% <100.00%> (+0.02%) :arrow_up:
@tanstack/react-query-devtools 10.00% <ø> (ø)
@tanstack/react-query-next-experimental ∅ <ø> (∅)
@tanstack/react-query-persist-client 100.00% <ø> (ø)
@tanstack/solid-query ∅ <ø> (∅)
@tanstack/solid-query-devtools ∅ <ø> (∅)
@tanstack/solid-query-persist-client ∅ <ø> (∅)
@tanstack/svelte-query ∅ <ø> (∅)
@tanstack/svelte-query-devtools ∅ <ø> (∅)
@tanstack/svelte-query-persist-client ∅ <ø> (∅)
@tanstack/vue-query ∅ <ø> (∅)
@tanstack/vue-query-devtools ∅ <ø> (∅)
:rocket: New features to boost your workflow:
  • :snowflake: Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • :package: JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

codecov[bot] avatar May 01 '25 13:05 codecov[bot]

Thank you for reviewing my PR. I thought queryOptions and mutationOptions could be structured similarly since it was an options-related function. I re-created useMutation as start. I changed it to only have UseMutationOptions, excluding unnecessary data tags and initialData.

Ubinquitous avatar May 01 '25 13:05 Ubinquitous

I merged https://github.com/TanStack/query/pull/9094 to resolve below ci failure because of flaky timer tests

image

manudeli avatar May 01 '25 16:05 manudeli

@Ubinquitous Resolve eslint error please

manudeli avatar May 06 '25 19:05 manudeli

Sorry, I fix error that 'should not allow excess properties' test don't have assertions

Ubinquitous avatar May 07 '25 03:05 Ubinquitous

Really looking forward to this PR!

Its really helpful to add reusable optimistic updates, rollbacks etc callbacks for mutations. But what if we want to use these reusable callbacks but also want a specific side effect in one component on onSucces, like on login I want to redirect. Is there a way to merge these callbacks somehow?

andredewaard avatar May 13 '25 08:05 andredewaard

Thank you, @andredewaard.

I'm not sure if I understood it correctly, but it seems like this can basically be implemented using the spread operator. I’ve used a similar pattern when working with queryOptions as well. Is this what you were referring to?

const options = mutationOptions({
  ...
})

useMutation({
  ...options,
  onSuccess: () => {},
})

If you're only changing the funnel in a specific component, I recommend wrapping it in a function for better management. I used to do it that way as well when working with queryOptions.

Alternatively, if you're using a function, you could also add logic to branch only when a specific component name is passed in.

export const postQueries = {
  all: () => ['post-list'],
  list: (): UseQueryOptions<Post[], Error, string> =>
    queryOptions({
      queryKey: [...postListQueries.all()],
      queryFn: () => getPostList(),
      select: (data) => getPostListString(data),
    }),
  detail: (type: PostType): UseSuspenseQueryOptions<PostInfo, Error> =>
    queryOptions({
      queryKey: [...postListQueries.all(), 'detail', type],
      queryFn: () => getPostDetail({ type }),
      select: (data) => data,
    }),
  ...
};

Ubinquitous avatar May 13 '25 08:05 Ubinquitous

@Ubinquitous Thanks for your reply. I meant to not override the logic happening in onSuccess if I add an onSuccess callback in the component.

So more like this.

useMutation({
  ...options,
  onSuccess: () => {
    options.onSuccess()
    // added logic
  },
})

andredewaard avatar May 13 '25 08:05 andredewaard

yes, with optional chaining on the options callback:

useMutation({
  ...options,
  onSuccess: () => {
    options?.onSuccess()
    // added logic
  },
})

TkDodo avatar May 13 '25 08:05 TkDodo

To override queryOptions without using the spread operator twice, you can use a prop getter.

const compose = (...functions) => (...args) =>
	functions.forEach((fn) => fn?.(...args))

const options = queryOptions({ ... })

const getOptions = ({ onSuccess }) => {
  return {
    onSuccess: compose(onSuccess, options.onSuccess),
    ...options
  }
}

getOptions({
  onSuccess: () => {} // behaves the same
})

Ubinquitous avatar May 13 '25 09:05 Ubinquitous

I find mutationOptions useful. Currently, I am using a style that manages queryKeys and mutationKeys by bundling them into javascript objects, and mutationOptions can reduce developers' type mistakes and boilerplate code when using it this way. In addition, it can be used with useMutation, useIsMutating, and queryClient.isMutating.

Just like queryOptions allows call invalidateQueries and use that variable, wouldn't mutationOptions do the same (at useMutation, useIsMutating), keeping the code clean?

export const QUERY_KEY = (uniqueKey) => {
  return {
    get: [...queries.all(), 'get-key', uniqueKey],
    list: [...queries.all(), 'list-key', uniqueKey],
  };
};

export const queries = {
  all: () => ['query-key'],
  get: ({ some }) =>
    queryOptions({
      queryKey: QUERY_KEY(some).get,
      queryFn: () => ...,
    }),
  list: ({ some }) =>
    queryOptions({
      queryKey: QUERY_KEY(some).list,
      queryFn: () => ...,
    }),
  // this
  update: (): UseMutationOptions<
    null,
    Error,
    Request
  > => ({
    mutationFn: (req) => ...,
  }),
};

Ubinquitous avatar May 20 '25 08:05 Ubinquitous

and mutationOptions can reduce developers' type mistakes and boilerplate code when using it this way

you can achieve the same thing (avoiding type mistakes) by using satisfies UseMutationOptions

it can be used with useMutation, useIsMutating, and queryClient.isMutating.

it will only work with things like isMutating if we make the MutationKey required. It’s not required for useMutation though. That’s the big difference to queries.#

I fear that if we make this helper, it should have mutationKey required, or it won’t do anything with functions that accept mutationFilters.

But if we do that, a lot of people will ask why all a sudden they have to give every mutation a MutationKey when only using it for useMutation ...

TkDodo avatar May 29 '25 15:05 TkDodo

you can achieve the same thing (avoiding type mistakes) by using satisfies UseMutationOptions

okay, it actually needs to be satisfies UseMutationOptions<any, any, any, any>, and that won’t work for inference that well:

https://www.typescriptlang.org/play/?ssl=6&ssc=3&pln=6&pc=51#code/JYWwDg9gTgLgBAbwKoGcCmBZArjAhjYCAOwHkwDiUAaOLdbPCogXzgDMoIQ4ByAATxEUeAMYBrAPRQ0uETAC0ARyxooATx4BYAFChIsOLgAehaojgw1YNHACCJiCgCiUTlDisOXXsdNbtOiKU8BDkhEJwALyIOnAgOPjhAGJEAFyGKGpEInAAFEbpRFggAEaqAJRRAHxwAMowUMBEAOb55TqsKIkobMBoKHComAlMZEwoADy4RGo007OGM3MzVTqBweYAJvi4HlG09CPhuaHj7doSEnDXNwB6APxAA

guess we need that helper after all

TkDodo avatar May 29 '25 15:05 TkDodo

any ETA on this? would be really helpful in my current app.

andredewaard avatar Jun 20 '25 13:06 andredewaard

@Nick-Lucas Thanks for idea suggestion to solve mutationKey optional problem! I added you as co-author in 2f7eb30

image

manudeli avatar Jun 29 '25 07:06 manudeli

I fixed the part where the test code was incorrect and added test code to check that useMutation works properly when there is no mutationKey.

Ubinquitous avatar Jul 07 '25 07:07 Ubinquitous

Thanks everyone who worked on this ❤️

TkDodo avatar Jul 09 '25 14:07 TkDodo

is there a plan to port this to solid-query?

binajmen avatar Jul 10 '25 05:07 binajmen