Mutations are used for creating, updating, or deleting data. Unlike queries, mutations are typically used to perform side effects on the server.
Basic Mutation
Use the useMutation hook to perform mutations:
import { useMutation } from '@tanstack/react-query'
function CreateTodo() {
const mutation = useMutation({
mutationFn: async (newTodo) => {
const response = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
headers: { 'Content-Type': 'application/json' },
})
return response.json()
},
})
return (
<div>
{mutation.isPending ? (
'Creating todo...'
) : (
<>
{mutation.isError && <div>Error: {mutation.error.message}</div>}
{mutation.isSuccess && <div>Todo created!</div>}
<button
onClick={() => {
mutation.mutate({ title: 'New Todo' })
}}
>
Create Todo
</button>
</>
)}
</div>
)
}
Mutation Results
The mutation result contains state and functions:
const {
// Data
data, // The last successfully resolved data
error, // The error object if mutation failed
variables, // The variables passed to mutationFn
// Status
status, // 'idle' | 'pending' | 'error' | 'success'
isPending, // Is the mutation currently pending?
isSuccess, // Did the mutation succeed?
isError, // Did the mutation fail?
isIdle, // Is the mutation idle?
isPaused, // Is the mutation paused?
// Functions
mutate, // Trigger the mutation
mutateAsync, // Trigger and return a promise
reset, // Reset mutation state
// Metadata
submittedAt, // Timestamp when mutation was submitted
failureCount, // Number of consecutive failures
failureReason, // The error from last failed attempt
} = useMutation({ mutationFn: createTodo })
Mutation vs MutateAsync
const mutation = useMutation({
mutationFn: createTodo,
})
// Fire and forget - errors are caught internally
mutation.mutate({ title: 'New Todo' })
const mutation = useMutation({
mutationFn: createTodo,
})
try {
// Returns a promise - you must handle errors
const data = await mutation.mutateAsync({ title: 'New Todo' })
console.log(data)
} catch (error) {
console.error(error)
}
With mutateAsync, you must handle errors yourself. Unhandled promise rejections will not be caught by TanStack Query’s error handling.
Mutation Side Effects
Execute callbacks at different stages of the mutation:
useMutation({
mutationFn: createTodo,
onMutate: async (variables) => {
// Called before mutation function
console.log('Creating todo:', variables)
// Optionally return context for rollback
return { id: Date.now() }
},
onSuccess: (data, variables, context) => {
// Called on success
console.log('Todo created:', data)
},
onError: (error, variables, context) => {
// Called on error
console.error('Failed to create todo:', error)
},
onSettled: (data, error, variables, context) => {
// Always called after success or error
console.log('Mutation completed')
},
})
Invalidating Queries
Invalidate and refetch queries after mutations:
import { useMutation, useQueryClient } from '@tanstack/react-query'
function CreateTodo() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: createTodo,
onSuccess: () => {
// Invalidate and refetch todos query
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
}
invalidateQueries marks queries as stale and triggers a refetch if they are currently being rendered.
Optimistic Updates
Update the UI before the mutation completes:
function UpdateTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos'] })
// Snapshot previous value
const previousTodos = queryClient.getQueryData(['todos'])
// Optimistically update
queryClient.setQueryData(['todos'], (old) => {
return old.map((todo) =>
todo.id === newTodo.id ? newTodo : todo
)
})
// Return context with snapshot
return { previousTodos }
},
onError: (err, newTodo, context) => {
// Rollback on error
queryClient.setQueryData(['todos'], context.previousTodos)
},
onSettled: () => {
// Refetch after error or success
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
}
Cancel Queries
Cancel any outgoing refetches to prevent them from overwriting optimistic updates.
Snapshot
Save the current query data to enable rollback on error.
Update
Optimistically update the cache with the expected result.
Error Handling
Rollback to the snapshot if the mutation fails.
Refetch
Always refetch to ensure data consistency.
Optimistic UI Updates
Show optimistic state in the UI without updating the cache:
function TodoList() {
const [text, setText] = React.useState('')
const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
const addTodoMutation = useMutation({
mutationFn: async (newTodo) => {
const response = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text: newTodo }),
headers: { 'Content-Type': 'application/json' },
})
if (!response.ok) throw new Error('Failed')
return response.json()
},
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})
return (
<div>
<form onSubmit={(e) => {
e.preventDefault()
addTodoMutation.mutate(text)
setText('')
}}>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button disabled={addTodoMutation.isPending}>Create</button>
</form>
<ul>
{todos?.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
{addTodoMutation.isPending && (
<li style={{ opacity: 0.5 }}>{addTodoMutation.variables}</li>
)}
{addTodoMutation.isError && (
<li style={{ color: 'red' }}>
{addTodoMutation.variables}
<button onClick={() => addTodoMutation.mutate(addTodoMutation.variables)}>
Retry
</button>
</li>
)}
</ul>
</div>
)
}
Mutation Updates from Responses
Update the cache with the mutation response:
const mutation = useMutation({
mutationFn: createTodo,
onSuccess: (newTodo) => {
// Add the new todo to the cache
queryClient.setQueryData(['todos'], (oldTodos) => {
return [...oldTodos, newTodo]
})
},
})
Sequential Mutations
Execute mutations one after another:
const createMutation = useMutation({ mutationFn: createTodo })
const updateMutation = useMutation({ mutationFn: updateTodo })
async function createAndUpdate() {
const newTodo = await createMutation.mutateAsync({ title: 'New' })
await updateMutation.mutateAsync({ ...newTodo, completed: true })
}
Mutation Retry
Mutations do not retry by default, but you can enable it:
useMutation({
mutationFn: createTodo,
retry: 3, // Retry up to 3 times
})
// Conditional retry
useMutation({
mutationFn: createTodo,
retry: (failureCount, error) => {
// Retry on network errors only
return error.message.includes('network') && failureCount < 3
},
})
Mutation Scopes
Control when mutations can run with mutation scope:
const mutation = useMutation({
mutationFn: updateTodo,
// This mutation will run even if other mutations are pending
scope: {
id: 'todo',
},
})
Persisting Mutations
Mutations can be persisted to storage and resumed:
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
const persister = createSyncStoragePersister({
storage: window.localStorage,
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
})
function App() {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister }}
>
{/* Your app */}
</PersistQueryClientProvider>
)
}
Global Mutation Callbacks
Set up global mutation callbacks in the QueryClient:
const queryClient = new QueryClient({
mutationCache: new MutationCache({
onSuccess: (data, variables, context, mutation) => {
console.log('Mutation succeeded:', mutation.options.mutationKey)
},
onError: (error, variables, context, mutation) => {
console.error('Mutation failed:', error)
// Show toast notification
},
}),
})
Mutation Keys
Optionally identify mutations with keys:
const mutation = useMutation({
mutationKey: ['createTodo'],
mutationFn: createTodo,
})
Use mutation keys to:
- Access mutation state with
useMutationState
- Filter mutations in global callbacks
- Cancel specific mutations
Accessing Mutation State
Access the state of mutations from anywhere:
import { useMutationState } from '@tanstack/react-query'
function PendingMutations() {
const pendingMutations = useMutationState({
filters: { status: 'pending' },
select: (mutation) => mutation.state.variables,
})
return (
<div>
{pendingMutations.length} mutations pending
</div>
)
}
Error Handling
Per Mutation
Global Default
Inline Callbacks
useMutation({
mutationFn: createTodo,
throwOnError: true, // Throw errors to error boundary
})
const queryClient = new QueryClient({
defaultOptions: {
mutations: {
throwOnError: true,
},
},
})
mutation.mutate(
{ title: 'New Todo' },
{
onSuccess: (data) => console.log(data),
onError: (error) => console.error(error),
}
)
Attach metadata to mutations:
useMutation({
mutationFn: createTodo,
meta: {
operation: 'create',
},
})
Access meta in callbacks:
const queryClient = new QueryClient({
mutationCache: new MutationCache({
onSuccess: (data, variables, context, mutation) => {
console.log(mutation.meta?.operation)
},
}),
})