install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/tanstack-mutation-patterns" ~/.claude/skills/intense-visions-harness-engineering-tanstack-mutation-patterns && rm -rf "$T"
manifest:
agents/skills/claude-code/tanstack-mutation-patterns/SKILL.mdsource content
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
for all data mutations — do not useuseMutation
for operations that modify server state.useQuery - Wrap
in a custom hook per mutation type — exposeuseMutation
andmutate
to consumers.isPending - Use
to trigger side effects after a confirmed server success: cache invalidation, navigation, success toasts.onSuccess - Use
to handle failures: error toasts, logging, form error display. Do not roll back cache here unless you implemented optimistic updates.onError - Use
for cleanup that must happen regardless of outcome (re-enabling buttons, hiding progress).onSettled - Pass
as the argument tovariables
— they are typed by themutate()
's parameter type.mutationFn - Use
instead ofmutateAsync
when you need tomutate
the result and handle errors with try/catch.await - Set
on mutations that should not retry (form submissions, payments) — unlike queries, mutation retry isretry: false
by default.0
// 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.
vs mutate
: 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).
vs isPending
: In TanStack Query v5, isLoading
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.