-
|
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 APIWhat 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.
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
English is not my native language, so please excuse any awkward phrasing :) |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
your user-land implementation also violates the rules of hooks. If the passed-in I don’t think this is worth pursuing because the prime use-case for Also, I don’t usually see the problem with multiple |
Beta Was this translation helpful? Give feedback.
your user-land implementation also violates the rules of hooks. If the passed-in
mutationsare a dynamic array, it will error at runtime.I don’t think this is worth pursuing because the prime use-case for
useQueriesis 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 touseMutationeven if you only need one of them.Also, I don’t usually see the problem with multiple
useMutationcalls in the same component. That usually means you have one “stateful” comp…