Skip to main content

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.

  1. State Management: It tracks the lifecycle of an action (isLoading, isSuccess, isError) and syncs this state with the global mutateCache.
  2. Reactivity: Like useFetch, it uses useSyncExternalStore to ensure that any component observing the same mutationKey (via useIsMutating or another useMutation) updates immediately.
  3. Manual Trigger: Unlike useFetch, which can be automatic, useMutation returns a mutate function that gives you full control over when the action occurs.
  4. Suspense & Error Boundaries: It can optionally "throw" its internal promise or error, allowing you to use standard React Suspense and ErrorBoundary components 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 before and after hooks.
  • 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
Deprecated: suspense on useMutation

Setting 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 mutate promise resolves, call client.invalidate(key) on affected keys. It revalidates stale-while-revalidate, so Suspense consumers don't flash a fallback. Reserve refetch() 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 via useIsMutating (which is scoped to that key).
  • Leverage Lifecycle Hooks: Use before for pre-action setup and after for success/error side effects instead of complex useEffect logic.
  • Drive writes with transitions/actions, not Suspense: The suspense option on useMutation is deprecated. Use startTransition (with isPending) or a <form action={mutate}>; keep Suspense for key-change reads.
  • Set appropriate error TTL: Ensure mutation errors persist long enough for the user to see them, but clear automatically to avoid stale feedback.
  • Use clearKeyAfterMutate for 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 mutate action function focused on the API call; move complex data transformations into service layers.