Mutations are used to create, update, or delete data on your server. Unlike queries, mutations are typically used to modify server state and trigger side effects.
Mutation Basics
A mutation is created using the useMutation hook:
import { useMutation } from '@tanstack/react-query'
function CreatePost() {
const mutation = useMutation({
mutationFn: (newPost: { title: string; body: string }) => {
return fetch('https://api.example.com/posts', {
method: 'POST',
body: JSON.stringify(newPost),
headers: { 'Content-Type': 'application/json' },
}).then((res) => res.json())
},
})
return (
<button
onClick={() => {
mutation.mutate({ title: 'Hello', body: 'World' })
}}
>
Create Post
</button>
)
}
Mutation States
A mutation can be in one of the following states:
Status States
idle - The mutation is idle or fresh/reset
pending - The mutation is currently running
error - The mutation encountered an error
success - The mutation was successful
From mutation.ts:26-41, the mutation state structure is:
export interface MutationState<
TData = unknown,
TError = DefaultError,
TVariables = unknown,
TOnMutateResult = unknown,
> {
context: TOnMutateResult | undefined
data: TData | undefined
error: TError | null
failureCount: number
failureReason: TError | null
isPaused: boolean
status: MutationStatus
variables: TVariables | undefined
submittedAt: number
}
Derived Boolean States
The mutation result includes convenient boolean flags derived from mutationObserver.ts:145-159:
this.#currentResult = {
...state,
isPending: state.status === 'pending',
isSuccess: state.status === 'success',
isError: state.status === 'error',
isIdle: state.status === 'idle',
mutate: this.mutate,
reset: this.reset,
}
Mutation Functions
mutate
The mutate function is used to trigger the mutation. It’s fire-and-forget - errors are not thrown but passed to callbacks:
const mutation = useMutation({
mutationFn: createPost,
})
mutation.mutate(
{ title: 'Hello', body: 'World' },
{
onSuccess: (data) => {
console.log('Post created:', data)
},
onError: (error) => {
console.error('Failed to create post:', error)
},
},
)
mutateAsync
The mutateAsync function returns a Promise, allowing you to use async/await:
const mutation = useMutation({
mutationFn: createPost,
})
try {
const data = await mutation.mutateAsync({ title: 'Hello', body: 'World' })
console.log('Post created:', data)
} catch (error) {
console.error('Failed to create post:', error)
}
mutate calls the callbacks you pass to it, while mutateAsync doesn’t. If you need callbacks, use mutate. If you need the Promise behavior, use mutateAsync.
Side Effects
Mutations support several lifecycle callbacks for performing side effects:
onMutate
Called before the mutation function is fired. Useful for optimistic updates:
const mutation = useMutation({
mutationFn: updatePost,
onMutate: async (newPost) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: ['posts', newPost.id] })
// Snapshot the previous value
const previousPost = queryClient.getQueryData(['posts', newPost.id])
// Optimistically update to the new value
queryClient.setQueryData(['posts', newPost.id], newPost)
// Return context with the snapshot
return { previousPost }
},
onError: (err, newPost, context) => {
// Rollback on error
queryClient.setQueryData(['posts', newPost.id], context?.previousPost)
},
onSettled: () => {
// Always refetch after error or success
queryClient.invalidateQueries({ queryKey: ['posts'] })
},
})
onSuccess
Called when the mutation succeeds:
const mutation = useMutation({
mutationFn: createPost,
onSuccess: (data, variables, context) => {
console.log('Post created successfully:', data)
queryClient.invalidateQueries({ queryKey: ['posts'] })
},
})
onError
Called when the mutation encounters an error:
const mutation = useMutation({
mutationFn: createPost,
onError: (error, variables, context) => {
console.error('Failed to create post:', error)
toast.error('Failed to create post')
},
})
onSettled
Called when the mutation finishes, regardless of success or failure:
const mutation = useMutation({
mutationFn: createPost,
onSettled: (data, error, variables, context) => {
// Always refetch
queryClient.invalidateQueries({ queryKey: ['posts'] })
},
})
Callback Execution Order
Callbacks are executed in the following order, as implemented in mutationObserver.ts:161-219:
onMutate (before the mutation function)
- Mutation function executes
- Either:
onSuccess → onSettled (on success)
onError → onSettled (on error)
if (action?.type === 'success') {
try {
this.#mutateOptions.onSuccess?.(action.data, variables, onMutateResult, context)
} catch (e) {
void Promise.reject(e)
}
try {
this.#mutateOptions.onSettled?.(action.data, null, variables, onMutateResult, context)
} catch (e) {
void Promise.reject(e)
}
} else if (action?.type === 'error') {
try {
this.#mutateOptions.onError?.(action.error, variables, onMutateResult, context)
} catch (e) {
void Promise.reject(e)
}
try {
this.#mutateOptions.onSettled?.(undefined, action.error, variables, onMutateResult, context)
} catch (e) {
void Promise.reject(e)
}
}
Mutation Context
The mutation function context provides metadata about the mutation:
const context = {
client: this.#client,
meta: this.options.meta,
mutationKey: this.options.mutationKey,
} satisfies MutationFunctionContext
This context is passed to all callbacks, allowing you to access the QueryClient, metadata, and mutation key.
Resetting Mutations
You can reset a mutation back to its idle state:
const mutation = useMutation({
mutationFn: createPost,
})
mutation.reset() // Reset to idle state
From mutationObserver.ts:119-126:
reset(): void {
this.#currentMutation?.removeObserver(this)
this.#currentMutation = undefined
this.#updateResult()
this.#notify()
}
Retry Behavior
Unlike queries, mutations do not retry by default. You can configure retry behavior:
const mutation = useMutation({
mutationFn: createPost,
retry: 3, // Retry 3 times on failure
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
})
Persisting Mutations
Mutations can be persisted to storage and resumed later, useful for offline-first applications:
import { useMutation } from '@tanstack/react-query'
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
const mutation = useMutation({
mutationFn: createPost,
// This mutation will be persisted and can be resumed
})
Use the resumePausedMutations method on the QueryClient to resume paused mutations when the app comes back online.
Optimistic Updates
Optimistic updates allow you to update the UI before the mutation completes:
const mutation = useMutation({
mutationFn: (updatedTodo) => axios.patch(`/api/todos/${updatedTodo.id}`, updatedTodo),
onMutate: async (updatedTodo) => {
// 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 === updatedTodo.id ? updatedTodo : todo
)
})
return { previousTodos }
},
onError: (err, updatedTodo, context) => {
// Rollback to previous value on error
queryClient.setQueryData(['todos'], context.previousTodos)
},
onSettled: () => {
// Refetch to ensure server state
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
Mutation Scope
Each call to mutate creates a new mutation execution. The mutation state represents the most recent execution:
const mutation = useMutation({ mutationFn: createPost })
mutation.mutate({ title: 'Post 1' }) // First execution
mutation.mutate({ title: 'Post 2' }) // Second execution (state updates to this one)
The mutation observer always tracks the latest mutation execution. Previous executions are not stored in the observer state.
Type Safety
Mutations are fully type-safe:
type Post = { id: number; title: string; body: string }
type NewPost = Omit<Post, 'id'>
const mutation = useMutation<
Post, // TData - response type
Error, // TError - error type
NewPost, // TVariables - variables type
{ previousPosts?: Post[] } // TContext - context type
>({
mutationFn: async (newPost: NewPost): Promise<Post> => {
const response = await fetch('https://api.example.com/posts', {
method: 'POST',
body: JSON.stringify(newPost),
})
return await response.json()
},
onMutate: async (newPost) => {
const previousPosts = queryClient.getQueryData<Post[]>(['posts'])
return { previousPosts }
},
onError: (error, newPost, context) => {
// All parameters are properly typed
if (context?.previousPosts) {
queryClient.setQueryData(['posts'], context.previousPosts)
}
},
})
Global Mutation Callbacks
You can set up global mutation callbacks in the MutationCache:
const mutationCache = new MutationCache({
onSuccess: (data, variables, context, mutation) => {
console.log('Mutation succeeded:', mutation.options.mutationKey)
},
onError: (error, variables, context, mutation) => {
console.error('Mutation failed:', mutation.options.mutationKey, error)
},
})
const queryClient = new QueryClient({ mutationCache })