Dev Highlights

Доки по разработке

This project is maintained by teniryte

6. Мутации и оптимистичные апдейты

useMutation отвечает за запись: создание, обновление, удаление, запуск серверных команд.

Важная разница с запросами:

Именно поэтому после мутации вы обычно делаете одно из двух:

Базовый useMutation

const queryClient = useQueryClient()

const createTodoMutation = useMutation({
  mutationKey: ['todos', 'create'],
  mutationFn: createTodo,
  onSuccess: async () => {
    await queryClient.invalidateQueries({ queryKey: ['todos'] })
  },
})

Что стоит запомнить:

mutate и mutateAsync

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 напрямую.

Оптимистичное обновление через UI

Самый простой вариант: не трогать кеш, а показать временный элемент по 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'] })
  },
})

Это актуальный паттерн из официальной документации:

Когда инвалидировать, а когда писать в кеш

Инвалидировать лучше, если:

Писать в кеш лучше, если:

На практике часто используют гибрид:

useMutationState

Позволяет читать состояние мутаций в любом месте приложения.

const pendingTodos = useMutationState<string>({
  filters: {
    mutationKey: ['todos', 'create'],
    status: 'pending',
  },
  select: (mutation) => mutation.state.variables,
})

Подходит для:

Состояния mutation

Плюс полезные поля:

Ошибки и retry

Для мутаций retry нужен реже, чем для query.

Обычно:

Практические советы