Skip to main content

Overview

The MutationObserver class is used to subscribe to a mutation and receive reactive updates when the mutation’s state changes. It’s the foundation for framework-specific hooks like useMutation in React.

Constructor

const observer = new MutationObserver<TData, TError, TVariables, TOnMutateResult>(
  client: QueryClient,
  options: MutationObserverOptions<TData, TError, TVariables, TOnMutateResult>
)
client
QueryClient
required
The QueryClient instance
options
MutationObserverOptions
required
Options for the mutation observer
Example:
const observer = new MutationObserver(queryClient, {
  mutationFn: async (newTodo) => {
    const response = await fetch('/api/todos', {
      method: 'POST',
      body: JSON.stringify(newTodo),
    })
    return response.json()
  },
  onSuccess: (data) => {
    console.log('Todo created:', data)
  },
})

Methods

subscribe()

Subscribes to mutation updates.
const unsubscribe = observer.subscribe(
  listener: (result: MutationObserverResult<TData, TError, TVariables, TOnMutateResult>) => void
): () => void
listener
(result: MutationObserverResult<TData, TError, TVariables, TOnMutateResult>) => void
required
Callback function that receives mutation results
Returns: Function to unsubscribe Example:
const unsubscribe = observer.subscribe((result) => {
  console.log('Status:', result.status)
  console.log('Data:', result.data)
  console.log('Is pending:', result.isPending)
})

// Later, unsubscribe
unsubscribe()

getCurrentResult()

Returns the current result without subscribing.
const result = observer.getCurrentResult(): MutationObserverResult<TData, TError, TVariables, TOnMutateResult>
Returns: Current mutation result
MutationObserverResult
object

setOptions()

Updates the observer’s options.
observer.setOptions(
  options: MutationObserverOptions<TData, TError, TVariables, TOnMutateResult>
): void
options
MutationObserverOptions
required
New options for the observer
Example:
observer.setOptions({
  mutationFn: updatedMutationFn,
  onSuccess: (data) => {
    console.log('Success with new callback:', data)
  },
})

mutate()

Executes the mutation.
const promise = observer.mutate(
  variables: TVariables,
  options?: MutateOptions<TData, TError, TVariables, TOnMutateResult>
): Promise<TData>
variables
TVariables
required
Variables to pass to the mutation function
options
MutateOptions
Additional options for this specific mutation call
Returns: Promise that resolves with the mutation data or rejects with the error Example:
try {
  const data = await observer.mutate(
    { title: 'New Todo', completed: false },
    {
      onSuccess: (data) => {
        console.log('This todo was created:', data)
      },
    }
  )
  console.log('Created:', data)
} catch (error) {
  console.error('Failed to create todo:', error)
}

reset()

Resets the mutation to its initial idle state.
observer.reset(): void
Example:
observer.reset()
Resetting removes the observer from the current mutation. A new call to mutate() will create a new mutation instance.

Properties

options

The current options for the observer.
observer.options: MutationObserverOptions<TData, TError, TVariables, TOnMutateResult>

Type Parameters

  • TData - The type of data returned by the mutation function
  • TError - The type of error that can be thrown (defaults to DefaultError)
  • TVariables - The type of variables passed to the mutation function (defaults to void)
  • TOnMutateResult - The type of value returned by the onMutate callback

Usage Example

import { QueryClient, MutationObserver } from '@tanstack/query-core'

const queryClient = new QueryClient()

interface Todo {
  id: number
  title: string
  completed: boolean
}

interface NewTodo {
  title: string
  completed: boolean
}

const observer = new MutationObserver<Todo, Error, NewTodo>(
  queryClient,
  {
    mutationFn: async (newTodo) => {
      const response = await fetch('/api/todos', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newTodo),
      })
      if (!response.ok) throw new Error('Failed to create todo')
      return response.json()
    },
    onMutate: async (newTodo) => {
      // Optimistically update the UI
      const previousTodos = queryClient.getQueryData(['todos'])
      queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
      return { previousTodos }
    },
    onError: (error, newTodo, context) => {
      // Rollback on error
      if (context?.previousTodos) {
        queryClient.setQueryData(['todos'], context.previousTodos)
      }
    },
    onSuccess: (data) => {
      // Invalidate and refetch
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  }
)

// Subscribe to updates
const unsubscribe = observer.subscribe((result) => {
  if (result.isPending) {
    console.log('Creating todo...')
  } else if (result.isError) {
    console.error('Error:', result.error)
  } else if (result.isSuccess) {
    console.log('Todo created:', result.data)
  }
})

// Execute the mutation
try {
  const newTodo = await observer.mutate({
    title: 'Learn TanStack Query',
    completed: false,
  })
  console.log('Created todo:', newTodo)
} catch (error) {
  console.error('Failed:', error)
}

// Later, clean up
unsubscribe()

Optimistic Updates

The MutationObserver is commonly used for optimistic updates:
const observer = new MutationObserver(queryClient, {
  mutationFn: updateTodo,
  onMutate: async (updatedTodo) => {
    // Cancel outgoing refetches
    await queryClient.cancelQueries({ queryKey: ['todos', updatedTodo.id] })
    
    // Snapshot the previous value
    const previousTodo = queryClient.getQueryData(['todos', updatedTodo.id])
    
    // Optimistically update
    queryClient.setQueryData(['todos', updatedTodo.id], updatedTodo)
    
    // Return context with the snapshot
    return { previousTodo }
  },
  onError: (error, updatedTodo, context) => {
    // Rollback to the previous value
    if (context?.previousTodo) {
      queryClient.setQueryData(['todos', updatedTodo.id], context.previousTodo)
    }
  },
  onSettled: (data, error, updatedTodo) => {
    // Always refetch after error or success
    queryClient.invalidateQueries({ queryKey: ['todos', updatedTodo.id] })
  },
})

Build docs developers (and LLMs) love