Skip to main content

Mutation: Common Patterns & 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>
</>
);
}
Deprecated: suspense / errorBoundary on useMutation

Throwing 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.