Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Proposal: useMutations – A batch mutation hook #10002

Answered by TkDodo
ayden94 asked this question in Ideas
Discussion options

When working with multiple mutations in the same component, it is very common to write repetitive useMutation calls:

const { mutate: deleteChildrenAccount } = useMutation(
 AuthModule.deleteChildrenAccount(queryClient),
);
const { mutate: patchUserActivityStatus } = useMutation(
 AuthModule.patchUserActivityStatus(queryClient),
);
const { mutate: postSubscriptionToUser } = useMutation(
 SubscriptionModule.postSubscriptionToUser({
 child_user_id: user.id,
 count: 1,
 queryClient,
 }),
);
const { mutate: deleteSubscriptionFromUser } = useMutation(
 SubscriptionModule.deleteSubscriptionFromUser({
 child_user_id: user.id,
 count: 1,
 queryClient,
 }),
);

This is verbose, but more importantly, it often tempts developers to try abstraction patterns that violate the Rules of Hooks (e.g. calling useMutation inside loops or conditionals).

Because of this, I believe library-level support for batching mutations—similar to useQueries—would be safer and more ergonomic than encouraging ad-hoc abstractions at the application level.

Desired API

What I want to write is something like this:

const [
 { mutate: deleteChildrenAccount },
 { mutate: patchUserActivityStatus },
 { mutate: postSubscriptionToUser },
 { mutate: deleteSubscriptionFromUser },
] = useMutations({
 mutations: [
 AuthModule.deleteChildrenAccount(queryClient),
 AuthModule.patchUserActivityStatus(queryClient),
 SubscriptionModule.postSubscriptionToUser({
 child_user_id: user.id,
 count: 1,
 queryClient,
 }),
 SubscriptionModule.deleteSubscriptionFromUser({
 child_user_id: user.id,
 count: 1,
 queryClient,
 }),
 ],
});

This mirrors the mental model of useQueries, but for mutations.

Current Implementation (Userland Prototype)

Below is a userland implementation I experimented with.
It ensures:

  • Hooks rules are respected (no conditional or dynamic hook calls)
  • Each mutation preserves its full type (UseMutationResult)
  • Tuple inference works correctly (similar to useQueries)
  • Recursive type depth is capped to avoid TS instantiation depth errors
import {
 QueryClient,
 useMutation,
 type DefaultError,
 type MutationFunction,
 type UseMutationOptions,
 type UseMutationResult,
} from '@tanstack/react-query';
// useMutations - Batch mutation hook similar to useQueries
type MAXIMUM_DEPTH = 20;
type GetUseMutationResult<T> =
 T extends {
 mutationFn?: MutationFunction<infer TData, infer TVariables>;
 onError?: (
 error: infer TError,
 variables: unknown,
 context: unknown
 ) => unknown;
 onMutate?: (variables: unknown) => infer TContext;
 }
 ? UseMutationResult<
 TData,
 unknown extends TError ? DefaultError : TError,
 TVariables,
 TContext
 >
 : T extends UseMutationOptions<
 infer TData,
 infer TError,
 infer TVariables,
 infer TContext
 >
 ? UseMutationResult<TData, TError, TVariables, TContext>
 : UseMutationResult;
type MutationsResults<
 T extends ReadonlyArray<unknown>,
 TResults extends ReadonlyArray<unknown> = [],
 TDepth extends ReadonlyArray<number> = [],
> =
 TDepth['length'] extends MAXIMUM_DEPTH
 ? Array<UseMutationResult>
 : T extends []
 ? []
 : T extends [infer Head]
 ? [...TResults, GetUseMutationResult<Head>]
 : T extends [infer Head, ...infer Tails]
 ? MutationsResults<
 [...Tails],
 [...TResults, GetUseMutationResult<Head>],
 [...TDepth, 1]
 >
 : { [K in keyof T]: GetUseMutationResult<T[K]> };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useMutations<const T extends ReadonlyArray<any>>({
 mutations,
 queryClient,
}: {
 mutations: T;
 queryClient?: QueryClient;
}): MutationsResults<T> {
 // eslint-disable-next-line react-hooks/rules-of-hooks
 return mutations.map((options) =>
 useMutation(options, queryClient),
 ) as MutationsResults<T>;
}

Discussion Points

  • Would a useMutations hook be acceptable as a first-class API, similar to useQueries?
  • Are there design concerns around mutation lifecycles or cache interaction that make this problematic?
  • Is there a preferred pattern today for grouping multiple mutations without violating Hooks rules?

English is not my native language, so please excuse any awkward phrasing :)
I would love feedback from the maintainers and the community.

You must be logged in to vote

Hooks rules are respected (no conditional or dynamic hook calls)

your user-land implementation also violates the rules of hooks. If the passed-in mutations are a dynamic array, it will error at runtime.

I don’t think this is worth pursuing because the prime use-case for useQueries is to have dynamic queries (e.g. mapping over all users returned from one endpoint and making a request for each of them), and mutations just don’t have that use-case. You would also create a mutationObserver for each call to useMutation even if you only need one of them.

Also, I don’t usually see the problem with multiple useMutation calls in the same component. That usually means you have one "stateful" comp...

Replies: 1 comment 1 reply

Comment options

Hooks rules are respected (no conditional or dynamic hook calls)

your user-land implementation also violates the rules of hooks. If the passed-in mutations are a dynamic array, it will error at runtime.

I don’t think this is worth pursuing because the prime use-case for useQueries is to have dynamic queries (e.g. mapping over all users returned from one endpoint and making a request for each of them), and mutations just don’t have that use-case. You would also create a mutationObserver for each call to useMutation even if you only need one of them.

Also, I don’t usually see the problem with multiple useMutation calls in the same component. That usually means you have one "stateful" component and them pass mutate down to child components to actually trigger the mutation. Like, I would put the deleteChildrenAccount inside a <DeleteAccount /> component that renders a delete button and triggers the delete. So it wouldn’t be in the same component as postSubscriptionToUser.

You must be logged in to vote
1 reply
Comment options

Thanks for the detailed response — that makes sense.

Just to clarify my original intent: I’m aware that a user-land implementation like the one I posted violates the Rules of Hooks when the mutations array is dynamic. That was actually the motivation for suggesting a library-level API, rather than encouraging ad-hoc abstractions in application code.

I understand your point that mutations are fundamentally different from queries, and that useQueries is designed around a collection-style mental model that doesn’t really apply to mutations. Framing mutations as commands tied to specific UI components is a reasonable and consistent design choice.

It sounds like the recommended approach is to keep multiple useMutation calls explicit and rely on component decomposition or domain-specific hooks instead of trying to batch them under a single abstraction. That answers my main question.

Thanks again for taking the time to explain the reasoning behind this — I appreciate the insight into the design philosophy.

Answer selected by ayden94
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
Ideas
Labels
None yet
2 participants

AltStyle によって変換されたページ (->オリジナル) /