From harness-claude
Guides use of useMutation for server mutations with onSuccess/onError/onSettled callbacks, cache invalidation, and retry logic. For React components.
How this skill is triggered — by the user, by Claude, or both
Slash command
/harness-claude:tanstack-mutation-patternsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Execute server-side mutations with useMutation, lifecycle callbacks, and retry configuration
Execute server-side mutations with useMutation, lifecycle callbacks, and retry configuration
useMutation for all data mutations — do not use useQuery for operations that modify server state.useMutation in a custom hook per mutation type — expose mutate and isPending to consumers.onSuccess to trigger side effects after a confirmed server success: cache invalidation, navigation, success toasts.onError to handle failures: error toasts, logging, form error display. Do not roll back cache here unless you implemented optimistic updates.onSettled for cleanup that must happen regardless of outcome (re-enabling buttons, hiding progress).variables as the argument to mutate() — they are typed by the mutationFn's parameter type.mutateAsync instead of mutate when you need to await the result and handle errors with try/catch.retry: false on mutations that should not retry (form submissions, payments) — unlike queries, mutation retry is 0 by 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 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>
);
}
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.
https://tanstack.com/query/latest/docs/framework/react/guides/mutations
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeImplements optimistic updates for TanStack Query mutations: updates UI immediately on mutate and rolls back on error using onMutate, onError, and onSettled callbacks.
Provides TanStack Query v5 reference for React data fetching, caching, server state management using useQuery/useMutation hooks, QueryClient setup, optimistic updates, Next.js SSR/hydration, testing, TypeScript, and advanced patterns.
Expert TanStack Query guidance for React and Next.js apps covering asynchronous state management, data fetching, caching, mutations, optimistic updates, and SSR integration.