useMutation: Actions + Global Status + Cache Invalidation
Comprehensive Best Practices Guide
Introduction
This guide provides a detailed overview of best practices when using Archibald's useMutation hook for data-modifying operations like POST, PUT, and DELETE requests.
How it works
Under the hood, useMutation provides a declarative interface for executing asynchronous actions while integrating deeply with Archibald's global state.
- State Management: It tracks the lifecycle of an action (
isLoading,isSuccess,isError) and syncs this state with the globalmutateCache. - Reactivity: Like
useFetch, it usesuseSyncExternalStoreto ensure that any component observing the samemutationKey(viauseIsMutatingor anotheruseMutation) updates immediately. - Manual Trigger: Unlike
useFetch, which can be automatic,useMutationreturns amutatefunction that gives you full control over when the action occurs. - Suspense & Error Boundaries: It can optionally "throw" its internal promise or error, allowing you to use standard React
SuspenseandErrorBoundarycomponents for a cleaner UI architecture.
Why use useMutation?
- Global Awareness: Other parts of your app can react to a mutation in progress (e.g., a global saving spinner).
- Encapsulation: Keeps side effects (analytics, logging, cache invalidation) co-located with the action definition using
beforeandafterhooks. - Predictable Error Handling: Provides consistent error state management across the entire application.
Suspense, transitions & actions
useMutation is designed for writes. React's model for writes is transitions and form actions, not Suspense — so prefer those over throwing the mutation promise.
Revalidate without a fallback flash
After a successful mutation, revalidate the affected keys with client.invalidate(key) rather than client.refetch(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 instead of re-suspending to a fallback. refetch clears by default and therefore flashes the nearest Suspense boundary.
const { mutate } = useMutation();
const client = useDataClient();
const [isPending, startTransition] = useTransition();
function save(payload) {
startTransition(async () => {
await mutate(() => api.updateThing(payload));
await client.invalidate(['thing', payload.id]); // no fallback flash
});
}
Pending UI with isPending
useMutation returns isPending (an alias of isLoading) and a stable mutate identity, so it can be passed straight to <form action={mutate}> or memoized children. For a rejecting promise that a transition or error boundary can catch, pass { throwOnError: true } on the call.
Suspend on key changes, not on the mutation
Suspense still shines for reads driven by a changing key (pagination, filters). Change the key inside startTransition and let useSuspenseFetch hold the old UI while the new page loads:
const [page, setPage] = useState(1);
const [isPending, startTransition] = useTransition();
const { data } = useSuspenseFetch(['items', page], () => api.getItems(page));
// startTransition(() => setPage(p => p + 1)) keeps the current list visible + isPending true
suspense on useMutationSetting suspense: true on a mutation throws the mutation promise during render, which unmounts the form and loses its local state. It is deprecated and kept only for backwards compatibility. Use a transition or a form action instead.
See Also
For a detailed technical breakdown and additional implementation patterns, refer to the following resources:
Key Takeaways
- Invalidate related data (don't clear-refetch): Mutations change server state; after the
mutatepromise resolves, callclient.invalidate(key)on affected keys. It revalidates stale-while-revalidate, so Suspense consumers don't flash a fallback. Reserverefetch()for when you deliberately want to clear first. - Use stable
mutationKey: For critical global actions (like "Login" or "Add to Cart"), provide a clear key to allow global status tracking viauseIsMutating(which is scoped to that key). - Leverage Lifecycle Hooks: Use
beforefor pre-action setup andafterfor success/error side effects instead of complexuseEffectlogic. - Drive writes with transitions/actions, not Suspense: The
suspenseoption onuseMutationis deprecated. UsestartTransition(withisPending) or a<form action={mutate}>; keep Suspense for key-change reads. - Set appropriate
errorTTL: Ensure mutation errors persist long enough for the user to see them, but clear automatically to avoid stale feedback. - Use
clearKeyAfterMutatefor transient actions: For one-time pings or analytics events, clean up the cache automatically to save memory. - Avoid heavy logic in the component: Keep your
mutateaction function focused on the API call; move complex data transformations into service layers.