When to Use useMutation in React Query for Server-Side Mutations
Use useMutation when performing write operations (POST, PUT, PATCH, DELETE) that require optimistic UI updates, automatic cache invalidation, or built-in error handling.
The useMutation hook from TanStack Query (formerly React Query) provides the idiomatic approach for modifying server data in React applications. While useQuery handles read operations, useMutation manages write operations with lifecycle callbacks and cache synchronization. Choosing useMutation over manual fetch calls or useEffect patterns ensures your application maintains predictable state management and optimal user experience.
Why useMutation Outperforms Manual Data Manipulation
Manual fetch calls inside components or useEffect hooks lack the cache awareness and lifecycle management that useMutation provides. The hook exposes isLoading, isSuccess, and isError states for granular UI control while preventing memory leaks through automatic garbage collection when components unmount.
Unlike useQuery, which executes automatically on mount, useMutation runs only when explicitly called, preventing unwanted write operations during renders.
Key Scenarios for Choosing useMutation
Explicit Write Operations
When your component needs to create, update, or delete resources, useMutation clearly signals the intent to mutate data. This self-documenting API makes codebases more maintainable compared to generic fetch wrappers.
Optimistic UI Updates
The onMutate, onSuccess, onError, and onSettled callbacks enable optimistic updates that modify the cache before server confirmation. If the mutation fails, the onError callback rolls back changes using the context returned from onMutate.
Cache Invalidation
After successful mutations, calling queryClient.invalidateQueries or queryClient.setQueryData within onSuccess ensures stale data refreshes automatically without additional boilerplate.
Concurrency and Error Handling
Built-in retry logic and granular error callbacks simplify robust error handling. Each mutation instance tracks its own loading state, allowing you to disable buttons or show spinners per-operation.
Creating Resources with useMutation
The following example demonstrates creating a post with optimistic UI updates and cache synchronization:
import {useMutation, useQueryClient} from '@tanstack/react-query';
function useCreatePost() {
const queryClient = useQueryClient();
return useMutation(
async (newPost) => {
const res = await fetch('/api/posts', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(newPost),
});
if (!res.ok) throw new Error('Network response was not ok');
return res.json();
},
{
onMutate: async (newPost) => {
await queryClient.cancelQueries(['posts']);
const previous = queryClient.getQueryData(['posts']);
queryClient.setQueryData(['posts'], (old) => [...(old ?? []), newPost]);
return {previous};
},
onError: (err, newPost, context) => {
queryClient.setQueryData(['posts'], context?.previous);
},
onSettled: () => {
queryClient.invalidateQueries(['posts']);
},
},
);
}
Updating and Deleting with Optimistic UI
For PATCH operations, modify the existing cache entry immediately while preserving rollback capability:
function useUpdateTodo() {
const qc = useQueryClient();
return useMutation(
async ({id, ...updates}) => {
const res = await fetch(`/api/todos/${id}`, {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(updates),
});
if (!res.ok) throw new Error('Failed to update');
return res.json();
},
{
onMutate: async ({id, ...updates}) => {
await qc.cancelQueries(['todos']);
const previous = qc.getQueryData(['todos']);
qc.setQueryData(['todos'], (old) =>
old?.map((t) => (t.id === id ? {...t, ...updates} : t)),
);
return {previous};
},
onError: (err, variables, context) => {
qc.setQueryData(['todos'], context?.previous);
},
onSettled: () => {
qc.invalidateQueries(['todos']);
},
},
);
}
For DELETE operations, filter the removed item from the cache immediately:
function useDeleteUser() {
const qc = useQueryClient();
return useMutation(
async (userId) => {
const res = await fetch(`/api/users/${userId}`, {method: 'DELETE'});
if (!res.ok) throw new Error('Delete failed');
return userId;
},
{
onMutate: async (userId) => {
await qc.cancelQueries(['users']);
const previous = qc.getQueryData(['users']);
qc.setQueryData(['users'], (old) => old?.filter((u) => u.id !== userId));
return {previous};
},
onError: (err, userId, context) => {
qc.setQueryData(['users'], context?.previous);
},
onSettled: () => {
qc.invalidateQueries(['users']);
},
},
);
}
React Hook Architecture and Integration
Although useMutation belongs to the TanStack Query ecosystem, it integrates deeply with React's hook system. The React repository contains the foundational hook dispatcher implementation in packages/react/src/ReactHooks.js, which manages how custom hooks like useMutation resolve during component renders.
Fiber-level hook state handling occurs in packages/react/src/ReactFiberHooks.js, enabling React Query to track mutation states across re-renders. The current dispatcher reference lives in packages/react/src/ReactCurrentDispatcher.js, while internal symbols are exposed through packages/react/src/ReactSharedInternals.js. This architecture allows React Query to hook into React's rendering pipeline and provide automatic garbage collection when components unmount, canceling in-flight mutations to prevent memory leaks.
Summary
- Use
useMutationfor all server write operations (POST, PUT, PATCH, DELETE) instead of manual fetch oruseQuery. - Leverage
onMutate,onError, andonSettledcallbacks for optimistic UI updates with automatic rollback capabilities. - Call
queryClient.invalidateQueriesinonSuccessoronSettledto refresh stale data automatically. - Encapsulate mutation logic in custom hooks to maintain separation of concerns and component purity.
- Rely on React's hook architecture (as implemented in
ReactHooks.jsandReactFiberHooks.js) for automatic cleanup and state synchronization.
Frequently Asked Questions
What is the difference between useQuery and useMutation in React Query?
useQuery executes automatically on component mount to fetch data, while useMutation must be called manually to perform write operations. useMutation provides specialized callbacks for optimistic updates and error handling that useQuery lacks, as it is designed for read-only operations.
How does useMutation handle optimistic updates?
The hook exposes an onMutate callback that executes before the network request, allowing you to modify the cache immediately. Return the previous state from onMutate, and if the mutation fails, onError receives this context to roll back the optimistic change automatically.
Can useMutation be used for GET requests?
While technically possible, useMutation is not recommended for GET requests because it does not execute automatically on mount and lacks the caching and background refetching optimizations that useQuery provides for read operations.
How does useMutation prevent memory leaks?
React Query automatically cancels in-flight mutations when the calling component unmounts, preventing state updates on unmounted components. This integrates with React's fiber architecture (specifically the cleanup mechanisms in ReactFiberHooks.js) to ensure proper garbage collection.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →