Dev Highlights

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

This project is maintained by teniryte

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

useMutation управляет операциями записи (POST/PUT/PATCH/DELETE). В отличие от useQuery, результат мутации не кешируется автоматически, но вы можете синхронизировать его с Query Cache вручную. В v5 добавили массу тонкостей:

Базовый пример

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createTodo } from './api';

export function CreateTodoForm() {
  const queryClient = useQueryClient();
  const mutation = useMutation({
    mutationKey: ['createTodo'],
    mutationFn: createTodo,
    onSuccess: async () => {
      await queryClient.invalidateQueries({ queryKey: ['todos'], refetchType: 'active' });
    },
  });

  return (
    <form
      onSubmit={event => {
        event.preventDefault();
        const form = new FormData(event.currentTarget);
        mutation.mutate({ title: form.get('title') as string });
      }}
    >
      <input name="title" disabled={mutation.isPending} />
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? 'Сохраняем…' : 'Создать'}
      </button>
    </form>
  );
}

Что здесь важно

Жизненный цикл мутации

Все обработчики могут быть заданы глобально через QueryClient.

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

const mutation = useMutation({
  mutationFn: updateTodo,
  onMutate: async updated => {
    await queryClient.cancelQueries({ queryKey: ['todos'] });
    const previous = queryClient.getQueryData<Todo[]>(['todos']);
    queryClient.setQueryData(['todos'], old =>
      old?.map(todo => (todo.id === updated.id ? { ...todo, ...updated } : todo)),
    );
    return { previous };
  },
  onError: (_err, _vars, context) => {
    if (context?.previous) {
      queryClient.setQueryData(['todos'], context.previous);
    }
  },
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});

Тонкости оптимизма v5

mutate vs mutateAsync

await mutation.mutateAsync(formData);
toast.success('Задача обновлена');

Обновление кеша без инвалидции

useMutation({
  mutationFn: toggleTodo,
  onSuccess: (data) => {
    queryClient.setQueryData(['todo', data.id], data);
    queryClient.setQueriesData(
      { queryKey: ['todos'] },
      old => old?.map(todo => (todo.id === data.id ? data : todo)),
    );
  },
});

Это уменьшает количество запросов в сеть и делает UI мгновенным.

Когда всё же инвалидировать

meta и конфигурация

useMutation({
  mutationFn: ({ title }) => client.post('/todos', { title }),
  meta: { feature: 'todo-create' },
  onSuccess: (_data, _vars, _ctx, mutation) => {
    analytics.track('todo_created', mutation.meta);
  },
});

Где ещё полезен meta

Состояния мутации

Откат (Rollback) и очередь

Лучшие практики

Дополнительные паттерны

Глобальные индикаторы отправки

const pendingSaves = useMutationState({
  filters: { status: 'pending', mutationKey: ['todo', 'save'] },
});

return <Spinner hidden={!pendingSaves.length} />;

useMutationState подписывается на кэш мутаций, не требует проп-дриллинга и автоматически очищается по gcTime.

Каскадные мутации

const updateTodo = useMutation({ mutationKey: ['todos', 'update'], mutationFn: patchTodo });
const revalidateStats = useMutation({ mutationKey: ['stats', 'sync'], mutationFn: syncStats });

await updateTodo.mutateAsync(payload);
await revalidateStats.mutateAsync({ projectId: payload.projectId });

Управление очередью

Работа с AbortController

const mutation = useMutation({
  mutationFn: async (payload, { signal }) => {
    return client.post('/upload', payload, { signal });
  },
});

useEffect(() => {
  return () => mutation.reset(); // отменит активную загрузку при размонтировании формы
}, [mutation]);

Предзаполнение форм и связывание с query

Инструменты отладки