Setup
import { useMutation, useQueryClient } from '@tanstack/react-query'
export function useAddTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (title: string) => ({ id: Date.now(), title }),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
}
Core Patterns
Update from mutation response
import { useMutation, useQueryClient } from '@tanstack/react-query'
export function useSaveTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (todo: { id: string; title: string }) => todo,
onSuccess: (todo) => {
queryClient.setQueryData(['todo', todo.id], todo)
},
})
}
Track related mutations
import { useMutationState } from '@tanstack/react-query'
export function usePendingTodoTitles() {
return useMutationState<string>({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => mutation.state.variables as string,
})
}
Scope defaults by mutation key
import { QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient()
queryClient.setMutationDefaults(['addTodo'], {
mutationFn: async (title: string) => ({ id: Date.now(), title }),
})
Common Mistakes
HIGH Multiple mutate arguments
Wrong:
import { useMutation } from '@tanstack/react-query'
export function useUpdateTodo() {
return useMutation({
mutationFn: async (input: { id: string; title: string }) => input,
})
}
useUpdateTodo().mutate('1', 'Ship')
Correct:
import { useMutation } from '@tanstack/react-query'
export function useUpdateTodo() {
return useMutation({
mutationFn: async (input: { id: string; title: string }) => input,
})
}
useUpdateTodo().mutate({ id: '1', title: 'Ship' })
Mutation variables are one value; pass an object when multiple fields are needed.
Source: TanStack/query:docs/framework/react/guides/mutations.md
HIGH Per-call callback after unmount
Wrong:
import { useMutation } from '@tanstack/react-query'
export function useSave() {
return useMutation({ mutationFn: async (title: string) => title })
}
useSave().mutate('Ship', { onSuccess: () => console.log('saved') })
Correct:
import { useMutation } from '@tanstack/react-query'
export function useSave() {
return useMutation({
mutationFn: async (title: string) => title,
onSuccess: () => console.log('saved'),
})
}
Hook-level callbacks are tied to the mutation lifecycle; per-call callbacks may not run if the observer unmounts.
Source: TanStack/query:docs/framework/react/guides/mutations.md
HIGH Not awaiting invalidation
Wrong:
import { useMutation, useQueryClient } from '@tanstack/react-query'
export function useAddTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (title: string) => title,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})
}
Correct:
import { useMutation, useQueryClient } from '@tanstack/react-query'
export function useAddTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (title: string) => title,
onSuccess: async () =>
queryClient.invalidateQueries({ queryKey: ['todos'] }),
})
}
Returning the invalidation promise keeps the mutation pending until dependent data is refreshed.
Source: TanStack/query:docs/framework/react/guides/invalidations-from-mutations.md
See also: core/implement-optimistic-updates-and-cache-writes for mutation side effects that update the cache before the server returns.