Skip to main content
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' })
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'] })
    },
  })
}
1

Cancel Queries

Cancel any outgoing refetches to prevent them from overwriting optimistic updates.
2

Snapshot

Save the current query data to enable rollback on error.
3

Update

Optimistically update the cache with the expected result.
4

Error Handling

Rollback to the snapshot if the mutation fails.
5

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

useMutation({
  mutationFn: createTodo,
  throwOnError: true, // Throw errors to error boundary
})

Mutation Meta

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)
    },
  }),
})

Build docs developers (and LLMs) love