> Execute server-side mutations with useMutation, lifecycle callbacks, and retry configuration
Scanned 9/11/2026
Install to Claude Code
npx -y skills add Intense-Visions/harness-engineering --skill tanstack-mutation-patterns --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Tanstack Mutation Patterns?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/intense-visions-tanstack-mutation-patterns-harness-engineering)More formats (shields.io, HTML) on the badges page.
# 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
1. Use `useMutation` for all data mutations — do not use `useQuery` for operations that modify server state.
2. Wrap `useMutation` in a custom hook per mutation type — expose `mutate` and `isPending` to consumers.
3. Use `onSuccess` to trigger side effects after a confirmed server success: cache invalidation, navigation, success toasts.
4. Use `onError` to handle failures: error toasts, logging, form error display. Do not roll back cache here unless you implemented optimistic updates.
5. Use `onSettled` for cleanup that must happen regardless of outcome (re-enabling buttons, hiding progress).
6. Pass `variables` as the argument to `mutate()` — they are typed by the `mutationFn`'s parameter type.
7. Use `mutateAsync` instead of `mutate` when you need to `await` the result and handle errors with try/catch.
8. Set `retry: false` on mutations that should not retry (form submissions, payments) — unlike queries, mutation retry is `0` by default.
```typescript
// 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 onSubmit={handleSubmit}>
<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
1. Read the instructions and examples in this document.
2. Apply the patterns to your implementation, adapting to your specific context.
3. 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.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!