State Management

TanStack Query: мутации и optimistic

В Linear пользователь меняет статус задачи на 'Done', и галочка переключается мгновенно - до того, как сервер успел ответить. Это не иллюзия скорости сети, а оптимистичное обновление: кэш правят локально сразу, а запрос летит в фоне. Если сервер ответит ошибкой, изменение откатывается на прежнее значение, будто его и не было. Под капотом за это отвечают useMutation для записи и invalidateQueries для сверки кэша после. Без этой связки кэш чтения остаётся старым после изменения, и на экране висят данные, которых на сервере уже нет.

  • Linear, Todoist, Notion: переключение статуса, лайк, чекбокс срабатывают мгновенно за счёт оптимистичного обновления
  • Корзина в e-commerce: добавление товара отражается сразу, а откат при ошибке возвращает прежнее количество
  • Соцсети: лайк и подписка ставятся оптимистично, потому что успех почти гарантирован, а отзывчивость важнее
  • Админки: создание и удаление записи через useMutation, после которого invalidateQueries обновляет таблицу
  • Формы редактирования профиля: сохранение через мутацию с инвалидацией кэша профиля по успеху

Предварительные знания

  • Основы TanStack Query: useQuery, queryKey, кэш и staleTime
  • Понимание стратегии stale-while-revalidate и фонового refetch
  • Промисы и обработка ошибок async/await
  • TanStack Query: основы

useMutation и инвалидация

Чтение делает useQuery, а запись - useMutation. Мутация не кэшируется по ключу: она выполняет разовое действие (создать, обновить, удалить) и отдаёт статусы isPending, isError, isSuccess и колбэки onSuccess, onError, onSettled. Само по себе изменение на сервере не трогает кэш чтения - связь между записью и чтением приходится задать явно.

Запускают мутацию через mutate с аргументом, который попадёт в mutationFn. Пока запрос идёт, isPending равен true - на этом удобно блокировать кнопку, чтобы не отправить дважды. Результат и ошибку обрабатывают в колбэках, а не в then у вызова: так логика обновления кэша живёт рядом с самой мутацией, а не разъезжается по компонентам.

КолбэкКогда вызываетсяТипичное применение
onMutateДо отправки запросаОптимистично правят кэш, сохраняют снимок для отката
onSuccessСервер ответил успехомИнвалидация связанного кэша, уведомление
onErrorСервер ответил ошибкойОткат оптимистичного изменения, показ ошибки
onSettledВ любом исходе после завершенияФинальная инвалидация для сверки с сервером

Мутация не GET и не POST по сути. useMutation можно применить и к GET-действию: разница не в HTTP-методе, а в семантике. Чтение кэшируется и дедуплицируется по ключу, запись это разовое действие со своими колбэками, результат которого в кэш надо завести отдельно.

Чем роль useMutation отличается от роли useQuery?

invalidateQueries после мутации

После того как мутация изменила данные на сервере, кэш чтения остаётся старым: useQuery всё ещё держит снимок до изменения. Чтобы их сверить, связанный кэш помечают устаревшим вызовом invalidateQueries с нужным queryKey. Это не удаляет данные - оно помечает их stale и запускает фоновый refetch активных запросов с этим ключом. На экране данные на миг остаются прежними, затем тихо подменяются свежими с сервера.

invalidateQueries сопоставляет ключи по префиксу. Вызов с ['tasks'] инвалидирует и ['tasks'], и ['tasks', { project: 'web' }], и ['tasks', { status: 'open' }] - все запросы, чей ключ начинается с 'tasks'. Это удобно: одна инвалидация после создания задачи обновит и общий список, и все его отфильтрованные варианты разом.

Инвалидация по префиксу ключа - повод проектировать ключи иерархично. Если все запросы задач начинаются с 'tasks', а внутри идут фильтры объектом, одна инвалидация ['tasks'] покрывает любой их вариант без перечисления.

Что делает invalidateQueries({ queryKey: ['tasks'] }) после успешной мутации?

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

Инвалидация по успеху отзывчива, но всё же ждёт ответа сервера. Оптимистичное обновление идёт дальше: изменение применяют в кэш сразу, до ответа. В onMutate отменяют идущие запросы по ключу (чтобы они не затёрли локальную правку), сохраняют текущий снимок кэша и правят его локально. Пользователь видит результат мгновенно, а запрос только отправляется в фон.

Если сервер ответил ошибкой, onError возвращает сохранённый снимок - это и есть откат (rollback). Прежнее значение приходит из context, который вернул onMutate. В onSettled, в любом исходе, запускается инвалидация: даже после удачного оптимистичного обновления финальная сверка с сервером гарантирует, что кэш точно совпал с источником, а не остался на догадке клиента.

  1. onMutate: отменить идущие запросы по ключу, сохранить снимок кэша, применить изменение локально
  2. Запрос летит на сервер в фоне, интерфейс уже показывает новое значение
  3. onError при сбое: вернуть сохранённый снимок из context - откат к прежнему состоянию
  4. onSettled в любом исходе: invalidateQueries для финальной сверки кэша с сервером

Оптимизм уместен там, где успех почти гарантирован и важна мгновенная реакция: лайки, чекбоксы, добавление в список. Для платежей и других критичных операций честнее дождаться ответа сервера, а не показывать результат, который придётся откатывать у пользователя на глазах.

В оптимистичном обновлении сервер ответил ошибкой. Что обеспечивает корректный откат?

Связь с другими темами

Урок про изменение данных. Дальше тема раскрывается так:

  • TanStack Query: основы — Мутации меняют кэш, который ведёт useQuery: чтение и запись это две стороны одного кэша
  • Server-state: паттерны — Оптимистичные обновления переносятся на пагинацию, бесконечные списки и офлайн-очереди
  • GraphQL-кеш как состояние — В Apollo и urql кэш после мутации обновляют похоже: либо рефетч, либо прямая запись в кэш

Итог

  • useMutation выполняет изменение на сервере (создать, обновить, удалить) и отдаёт статусы isPending, isError и колбэки onSuccess, onError, onSettled. По ключу мутация не кэшируется
  • Само изменение на сервере не обновляет кэш чтения. После успешной мутации связанный кэш помечают устаревшим вызовом invalidateQueries, и Query запускает фоновый refetch
  • Оптимистичное обновление в onMutate правит кэш локально до ответа сервера и сохраняет прежнее значение для отката
  • При ошибке onError возвращает сохранённый снимок (rollback), а onSettled запускает инвалидацию, чтобы кэш в любом исходе сошёлся с сервером
  • Оптимизм уместен там, где успех почти гарантирован и важна отзывчивость (лайки, чекбоксы). Для платежей честнее дождаться ответа сервера

Связанные уроки

  • sm-31-tanstack-query — Мутации меняют тот самый кэш, который ведёт useQuery, поэтому сначала нужны основы Query
  • sm-35-server-state-patterns — Поверх мутаций строятся продвинутые паттерны: оптимистичная пагинация, офлайн-очереди
TanStack Query: мутации и optimistic

0

1

Войти