Доки по разработке
This project is maintained by teniryte
useMutation отвечает за запись: создание, обновление, удаление, запуск серверных команд.
Важная разница с запросами:
Именно поэтому после мутации вы обычно делаете одно из двух:
setQueryDatauseMutationconst queryClient = useQueryClient()
const createTodoMutation = useMutation({
mutationKey: ['todos', 'create'],
mutationFn: createTodo,
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
Что стоит запомнить:
mutationKey помогает в Devtools, useMutationState и общей телеметрии.Promise из onSuccess или onSettled, mutation будет считаться pending до завершения этой работы.mutate и mutateAsyncmutate удобен в обработчиках событий.mutateAsync удобен в async/await сценариях.await createTodoMutation.mutateAsync({ title })
toast.success('Created')
Если сервер возвращает обновлённый объект, не обязательно делать лишний refetch:
const updateTodoMutation = useMutation({
mutationFn: updateTodo,
onSuccess: (data, variables) => {
queryClient.setQueryData(['todos', 'detail', variables.id], data)
},
})
Это официальный и рекомендуемый паттерн для detail-записей.
queryClient.setQueryData(['todos', 'detail', id], (old) =>
old
? {
...old,
title: newTitle,
}
: old,
)
Никогда не мутируйте old напрямую.
Самый простой вариант: не трогать кеш, а показать временный элемент по variables.
const addTodoMutation = useMutation({
mutationKey: ['todos', 'create'],
mutationFn: (text: string) => axios.post('/api/todos', { text }),
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})
const optimisticText = addTodoMutation.variables
Такой подход хорош, если optimistic state нужен только в одном месте экрана.
Если optimistic state должен отразиться в нескольких местах, обновляйте кеш.
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo, context) => {
await context.client.cancelQueries({ queryKey: ['todos'] })
const previousTodos = context.client.getQueryData<Todo[]>(['todos'])
context.client.setQueryData<Todo[]>(['todos'], (old = []) =>
old.map((todo) =>
todo.id === newTodo.id ? { ...todo, ...newTodo } : todo,
),
)
return { previousTodos }
},
onError: (_error, _variables, onMutateResult, context) => {
context.client.setQueryData(['todos'], onMutateResult?.previousTodos)
},
onSettled: (_data, _error, variables, _onMutateResult, context) => {
return context.client.invalidateQueries({ queryKey: ['todos'] })
},
})
Это актуальный паттерн из официальной документации:
onMutate отменяем конкурирующие refetchonError откатываемonSettled синхронизируем с серверомИнвалидировать лучше, если:
Писать в кеш лучше, если:
На практике часто используют гибрид:
setQueryData для мгновенного UIinvalidateQueries для гарантии консистентностиuseMutationStateПозволяет читать состояние мутаций в любом месте приложения.
const pendingTodos = useMutationState<string>({
filters: {
mutationKey: ['todos', 'create'],
status: 'pending',
},
select: (mutation) => mutation.state.variables,
})
Подходит для:
mutateisIdleisPendingisSuccessisErrorisPausedПлюс полезные поля:
variablessubmittedAtfailureCountfailureReasonДля мутаций retry нужен реже, чем для query.
Обычно:
retry: 0networkMode: 'offlineFirst' и персистентностьmutationKey к доменной модели, а не к названию кнопки.