T
TanStack7mo ago
correct-apricot

mutationOptions with queryClient variables type inferrence

i made a wrapper function for mutations that lets you create mutationOptions with queryClient in it that you can use for optimistic queries and invalidation, etc... For some reason it can't infer the variables properly in onMutate, while when i make a wrapper without the queryClient it can:
import type { DefaultError, QueryClient, UseMutationOptions } from "@tanstack/react-query";

import { useQueryClient } from "@tanstack/react-query";

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

export function mutationOptionsWithClient<
TData = unknown,
TError = DefaultError,
TVariables = void,
TContext = unknown,
>(
getOptions: (queryClient: QueryClient) => UseMutationOptions<TData, TError, TVariables, TContext>,
): UseMutationOptions<TData, TError, TVariables, TContext> {
// eslint-disable-next-line react-hooks/rules-of-hooks
const queryClient = useQueryClient();

return getOptions(queryClient);
}
import type { DefaultError, QueryClient, UseMutationOptions } from "@tanstack/react-query";

import { useQueryClient } from "@tanstack/react-query";

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

export function mutationOptionsWithClient<
TData = unknown,
TError = DefaultError,
TVariables = void,
TContext = unknown,
>(
getOptions: (queryClient: QueryClient) => UseMutationOptions<TData, TError, TVariables, TContext>,
): UseMutationOptions<TData, TError, TVariables, TContext> {
// eslint-disable-next-line react-hooks/rules-of-hooks
const queryClient = useQueryClient();

return getOptions(queryClient);
}
4 Replies
correct-apricot
correct-apricotOP7mo ago
example usage:
export const updateTaskOptions = () =>
mutationOptionsWithClient((queryClient) => ({
mutationKey: ["tasks", "updateTask"],
mutationFn: updateTaskRequest,
onMutate: async (newTask) => {
await queryClient.cancelQueries({ queryKey: getTasksOptions().queryKey });

const previousTasks = queryClient.getQueryData(getTasksOptions().queryKey);

queryClient.setQueryData(getTasksOptions().queryKey, (old) => {
if (!old) return old;

return {
...old,
tasks: old.tasks.map((task) => (task.id === newTask.id ? newTask : task)),
};
});

return { previousTasks };
},
onError: (_error, _newTask, context) => {
queryClient.setQueryData(getTasksOptions().queryKey, context?.previousTasks);
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["tasks"] });
toast.success("Task updated successfully");
},
}));
export const updateTaskOptions = () =>
mutationOptionsWithClient((queryClient) => ({
mutationKey: ["tasks", "updateTask"],
mutationFn: updateTaskRequest,
onMutate: async (newTask) => {
await queryClient.cancelQueries({ queryKey: getTasksOptions().queryKey });

const previousTasks = queryClient.getQueryData(getTasksOptions().queryKey);

queryClient.setQueryData(getTasksOptions().queryKey, (old) => {
if (!old) return old;

return {
...old,
tasks: old.tasks.map((task) => (task.id === newTask.id ? newTask : task)),
};
});

return { previousTasks };
},
onError: (_error, _newTask, context) => {
queryClient.setQueryData(getTasksOptions().queryKey, context?.previousTasks);
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["tasks"] });
toast.success("Task updated successfully");
},
}));
if I remove onMutate, types get inferred correctly, but when I add it, types break with newTask being inferred as void and mutationFn arguments being inferred as void as well if I use the mutationOptions variant i made above, then everything (all generics) is properly inferred (though I dont have the queryClient instance to use)
adverse-sapphire
adverse-sapphire7mo ago
Aside from the type issues, using the useQueryClient() hook in a non-hook helper sounds like a bad idea And perhaps useful for your issue at hand, I'm using the following useMutation() wrapper that does seem to type the options (including onMutate) correctly, not sure if it helps you in any way
export function useGraphQLMutation<
TData,
TVariables,
TInputVariables = TVariables extends Exact<EmptyObject, TVariables> ? void : TVariables
>(
document: TadaDocumentNode<TData, TVariables>,
options?: UseMutationOptions<TData, RequestError, TInputVariables>
) {
return useRQMutation<TData, RequestError, TInputVariables>({
// @ts-expect-error - Hard to type optionality of the variables correctly.
mutationFn: (variables) => fetchDocument(document, variables),
...options
});
}
export function useGraphQLMutation<
TData,
TVariables,
TInputVariables = TVariables extends Exact<EmptyObject, TVariables> ? void : TVariables
>(
document: TadaDocumentNode<TData, TVariables>,
options?: UseMutationOptions<TData, RequestError, TInputVariables>
) {
return useRQMutation<TData, RequestError, TInputVariables>({
// @ts-expect-error - Hard to type optionality of the variables correctly.
mutationFn: (variables) => fetchDocument(document, variables),
...options
});
}
correct-apricot
correct-apricotOP7mo ago
ye i know, though i always call it inside useMutation so its not a problem sadly no, the issue is that the queryClient seems to modify the type inside the function which is why I get the void type for variables in the end, I'll probably just go back to extracting the useMutation hook into a custom hook for each mutation, though the idea was to have queryClient always accessible as a parameter in the callback, without having to import and call useQueryClient
adverse-sapphire
adverse-sapphire7mo ago
Yeah - I tried various things like that too initially but it's a lot of hassle to get that right, gave up in the end

Did you find this page helpful?