Mutation: Common Patterns & Best Practices
- Basic Mutation
- Revalidating After a Mutation
- Shared Mutation State
- Transitions & Form Actions
- Resetting Error State
- Best Practices
Basic Mutation
The most common use case for useMutation is performing a single action like submitting a form or deleting an item.
function AddTodo() {
const { mutate, isLoading } = useMutation();
const handleSubmit = async (text: string) => {
await mutate(() => api.addTodo({ text }));
console.log('Todo added!');
};
return (
<button onClick={() => handleSubmit('New Todo')} disabled={isLoading}>
{isLoading ? 'Adding...' : 'Add Todo'}
</button>
);
}
Revalidating After a Mutation
Unlike useFetch, useMutation does not automatically update other cached data. After a successful mutation, revalidate the affected keys with client.invalidate(key). invalidate is stale-while-revalidate: it refetches without clearing the cached data first, so any useFetch/useSuspenseFetch reading that key keeps its current content on screen while the fresh data loads — no fallback flash. Reserve refetch() for when you deliberately want to clear first.
function TodoList() {
const { data } = useFetch('todos', () => api.getTodos());
const { mutate } = useMutation();
const client = useDataClient();
const handleDelete = async (id: string) => {
await mutate(() => api.deleteTodo(id));
// Stale-while-revalidate: keep the list on screen while it refreshes.
await client.invalidate('todos');
};
return (
<ul>
{data?.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => handleDelete(todo.id)}>Delete</button>
</li>
))}
</ul>
);
}
Shared Mutation State
By using a mutationKey, you can track the status of a mutation from different components.
// Component A: Initiates the mutation
function UpdateProfile() {
const { mutate } = useMutation({ mutationKey: 'user-update' });
// ...
}
// Component B: Displays a global loading indicator
function GlobalLoading() {
const isMutating = useIsMutating('user-update');
if (!isMutating) return null;
return <div className="toast">Updating profile...</div>;
}
Transitions & Form Actions
useMutation is for writes, and React's model for writes is transitions and form actions — not Suspense. mutate has a stable identity and the hook exposes isPending (an alias of isLoading), so it drops straight into startTransition or <form action={mutate}>. Pass { throwOnError: true } on the call for a rejecting promise a transition or error boundary can catch.
function ProfileForm({ data }) {
const { mutate, isPending, error } = useMutation();
const client = useDataClient();
const [isSaving, startTransition] = useTransition();
const save = () => {
startTransition(async () => {
await mutate(() => api.saveProfile(data), { throwOnError: true });
await client.invalidate(['profile', data.id]); // no fallback flash
});
};
return (
<>
{error && <p role="alert">Something went wrong!</p>}
<button onClick={save} disabled={isSaving || isPending}>Save</button>
</>
);
}
suspense / errorBoundary on useMutationThrowing the mutation promise during render unmounts the form and loses its local state. The suspense (and dependent errorBoundary) options are deprecated — use a transition or a form action as above. Keep Suspense for key-change reads (pagination/filters) via useSuspenseFetch.
Resetting Error State
Use resetError to allow users to recover from a failed mutation without re-running it immediately.
function SearchAction() {
const { mutate, error, resetError, isError } = useMutation();
if (isError) {
return (
<div className="error">
Failed: {error.message}
<button onClick={resetError}>Try again</button>
</div>
);
}
return <button onClick={() => mutate(doSearch)}>Search</button>;
}
Best Practices
Use clearKeyAfterMutate for transient actions
If a mutation is a one-time event (like a "Like" button or a tracking pixel) and you don't need to keep its result in memory, enable clearKeyAfterMutate.
Consistent Keys
Use stable keys for mutations that represent the same logical operation to avoid state fragmentation.
Avoid heavy logic in before/after
Keep your lifecycle hooks focused on side effects (analytics, logging, simple cache invalidation). Move complex business logic into the mutation action itself or into a dedicated service layer.
Use mutate return value
The mutate function returns a promise that resolves with the action result. You can use await to perform follow-up actions directly in your event handler.