Skip to main content
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:
  1. onMutate (before the mutation function)
  2. Mutation function executes
  3. Either:
    • onSuccessonSettled (on success)
    • onErroronSettled (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 })

Build docs developers (and LLMs) love