TanStack Query: Mutation Patterns
Execute server-side mutations with useMutation, lifecycle callbacks, and retry configuration
When to Use
- Creating, updating, or deleting data on the server from a Client Component
- Coordinating side effects (cache updates, navigation, toasts) after a mutation succeeds or fails
- Implementing retry logic for transient network failures
- Sharing mutation logic across multiple components via custom hooks
Instructions
- Use
useMutationfor all data mutations — do not useuseQueryfor operations that modify server state. - Wrap
useMutationin a custom hook per mutation type — exposemutateandisPendingto consumers. - Use
onSuccessto trigger side effects after a confirmed server success: cache invalidation, navigation, success toasts. - Use
onErrorto handle failures: error toasts, logging, form error display. Do not roll back cache here unless you implemented optimistic updates. - Use
onSettledfor cleanup that must happen regardless of outcome (re-enabling buttons, hiding progress). - Pass
variablesas the argument tomutate()— they are typed by themutationFn's parameter type. - Use
mutateAsyncinstead ofmutatewhen you need toawaitthe result and handle errors with try/catch. - Set
retry: falseon mutations that should not retry (form submissions, payments) — unlike queries, mutation retry is0by default.
// hooks/use-create-post.ts — mutation custom hook
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { postKeys } from '@/queries/posts';
interface CreatePostInput {
title: string;
content: string;
published: boolean;
}
export function useCreatePost() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (input: CreatePostInput) =>
fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
}).then(async r => {
if (!r.ok) throw new Error(await r.text());
return r.json() as Promise<Post>;
}),
onSuccess: (newPost) => {
// Seed the detail cache — no extra fetch needed
queryClient.setQueryData(postKeys.detail(newPost.id), newPost);
// Invalidate lists — server determines sort order and filtering
queryClient.invalidateQueries({ queryKey: postKeys.lists() });
toast.success('Post created');
},
onError: (error) => {
toast.error(`Failed to create post: ${error.message}`);
},
});
}
// components/create-post-form.tsx — consuming the mutation
export function CreatePostForm() {
const { mutate, isPending, isError, error } = useCreatePost();
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = new FormData(e.currentTarget);
mutate({
title: data.get('title') as string,
content: data.get('content') as string,
published: data.get('published') === 'on',
});
};
return (
<form
<input name="title" required />
<textarea name="content" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create Post'}
</button>
{isError && <p className="text-red-500">{error.message}</p>}
</form>
);
}
Details
useMutation is TanStack Query's API for server-side mutations. Unlike useQuery, it does not run automatically on mount — it runs when mutate() or mutateAsync() is called.
mutate vs mutateAsync: mutate is fire-and-forget — errors are handled in onError, not thrown. mutateAsync returns a Promise that rejects on error, enabling async/await with try/catch. Use mutateAsync when you need sequential async operations after the mutation (e.g., navigate then show toast in a specific order).
Lifecycle callback order: For a successful mutation: onMutate → (request) → onSuccess → onSettled. For a failed mutation: onMutate → (request) → onError → onSettled. Callbacks at the useMutation definition level fire first; callbacks at the mutate() call site fire after.
Variables type inference: TypeScript infers the type of variables from the mutationFn parameter. The variables object is available in all lifecycle callbacks — use it to know which item was mutated in onSuccess when invalidating specific keys.
Global mutation callbacks: Register onSuccess, onError, and onSettled on the MutationCache in QueryClient options for cross-cutting concerns (global error logging, analytics).
isPending vs isLoading: In TanStack Query v5, isLoading was renamed to isPending for mutations (and means the mutation is currently executing). Use isPending to disable submit buttons.
Source
https://tanstack.com/query/latest/docs/framework/react/guides/mutations
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.