Ручное обновление кэша

Структура кэша и ключи запросов

Кэш RTK Query строится вокруг endpoint + аргументы запроса. Каждый вызов хука формирует уникальный ключ, по которому сохраняются данные:

  • getPosts() → один кэш-ключ
  • getPosts({ page: 1 }) → другой кэш-ключ
  • getPosts({ page: 2 }) → третий кэш-ключ

Ключ формируется через сериализацию аргументов, поэтому даже логически одинаковые, но структурно разные объекты могут создавать разные записи в кэше.

Важно учитывать:

  • порядок полей в объекте может влиять на результат сериализации
  • примитивы и объекты обрабатываются по-разному
  • нормализация данных по умолчанию отсутствует (если не настроен transformResponse + entity adapter)

Базовые механизмы ручного обновления

RTK Query предоставляет несколько низкоуровневых инструментов для прямого вмешательства в кэш:

  • api.util.updateQueryData
  • api.util.invalidateTags
  • api.util.upsertQueryData
  • api.util.resetApiState

Каждый из них решает разные задачи: от точечного изменения записи до полного сброса состояния API-слайса.


updateQueryData: точечная мутация кэша

updateQueryData позволяет синхронно изменить данные в кэше конкретного запроса без повторного запроса на сервер.

Сигнатура:

dispatch(
  api.util.updateQueryData(endpointName, args, (draft) => {
    // мутирование draft (Immer)
  })
);

Принцип работы:

  • RTK Query находит кэш по endpointName + args
  • передаёт данные в Immer draft
  • изменения применяются иммутабельно

Пример обновления списка:

dispatch(
  api.util.updateQueryData('getPosts', undefined, (draft) => {
    const post = draft.find(p => p.id === 1);
    if (post) {
      post.title = 'Обновлённый заголовок';
    }
  })
);

Особенность:

  • изменения происходят мгновенно
  • запрос на сервер не выполняется
  • все подписанные компоненты перерисовываются

updateQueryData для вложенных аргументов

При работе с параметризованными запросами важно точно передавать аргументы:

dispatch(
  api.util.updateQueryData('getPosts', { page: 2 }, (draft) => {
    draft.items.push({
      id: 999,
      title: 'Новый пост'
    });
  })
);

Если аргументы не совпадают с кэшем, обновление не произойдёт.


upsertQueryData: создание или замена кэша

upsertQueryData используется для полной установки значения кэша.

dispatch(
  api.util.upsertQueryData('getPost', 1, {
    id: 1,
    title: 'Созданный или заменённый пост'
  })
);

Поведение:

  • если запись существует — перезаписывается
  • если отсутствует — создаётся новая

Это полезно при:

  • предзагрузке данных
  • восстановлении состояния из локального хранилища
  • синхронизации с websocket

Паттерн optimistic update через onQueryStarted

Один из ключевых сценариев ручного обновления кэша — оптимистические изменения.

createPost: builder.mutation({
  query: (body) => ({
    url: '/posts',
    method: 'POST',
    body
  }),
  async onQueryStarted(arg, { dispatch, queryFulfilled }) {
    const patchResult = dispatch(
      api.util.updateQueryData('getPosts', undefined, (draft) => {
        draft.unshift({
          id: Date.now(),
          ...arg
        });
      })
    );

    try {
      await queryFulfilled;
    } catch {
      patchResult.undo();
    }
  }
});

Ключевые моменты:

  • UI обновляется до ответа сервера
  • при ошибке выполняется rollback через undo()
  • состояние кэша остаётся консистентным

rollback изменений через undo

updateQueryData возвращает объект patch, содержащий метод отката:

const patchResult = dispatch(
  api.util.updateQueryData('getPosts', undefined, draft => {
    draft.push(newPost);
  })
);

// откат
patchResult.undo();

Это позволяет строить безопасные оптимистические сценарии без ручного хранения предыдущего состояния.


invalidateTags как форма косвенного обновления

Хотя это не прямое редактирование кэша, invalidateTags часто используется совместно с ручным управлением:

dispatch(
  api.util.invalidateTags(['Posts'])
);

Поведение:

  • все запросы с этим тегом считаются устаревшими
  • RTK Query автоматически перезапрашивает данные
  • ручное обновление кэша может использоваться до или после инвалидирования

Сочетание invalidateTags и updateQueryData

Распространённый гибридный подход:

dispatch(
  api.util.updateQueryData('getPosts', undefined, draft => {
    draft.unshift(tempPost);
  })
);

dispatch(
  api.util.invalidateTags(['Posts']);
);

Логика:

  • сначала мгновенно обновляется UI
  • затем запускается синхронизация с сервером
  • итоговое состояние подтверждается backend-данными

resetApiState: полная очистка кэша

Для полного сброса всех запросов:

dispatch(api.util.resetApiState());

Применяется в случаях:

  • logout пользователя
  • смена tenant / проекта
  • критическая рассинхронизация состояния

Поведение:

  • очищаются все endpoints
  • сбрасываются подписки
  • удаляются аргументированные кэши

Работа с несколькими кэш-ключами

При сложных интерфейсах часто требуется синхронное обновление нескольких записей:

dispatch(
  api.util.updateQueryData('getPost', 1, draft => {
    draft.title = 'Обновлено';
  })
);

dispatch(
  api.util.updateQueryData('getPosts', undefined, draft => {
    const item = draft.find(p => p.id === 1);
    if (item) item.title = 'Обновлено';
  })
);

Проблема:

  • дублирование логики
  • риск несогласованности

Решение:

  • централизованные функции обновления состояния
  • единый источник истины для трансформаций

Иммутабельность и Immer внутри кэша

RTK Query использует Immer, поэтому внутри updateQueryData допускается “мутационный” стиль:

draft.title = 'Новое значение';
draft.items.push(newItem);

Фактически:

  • создаётся структурное копирование
  • оригинальные данные не изменяются
  • обеспечивается безопасная интеграция с Redux

Типичные ошибки при ручном обновлении

Несовпадение аргументов запроса

updateQueryData('getPosts', { page: '1' }, ...)

и

getPosts({ page: 1 })

Это разные ключи.


Попытка обновить несуществующий кэш

  • если запрос не выполнялся, запись может отсутствовать
  • updateQueryData не создаёт кэш автоматически

Игнорирование нескольких подписчиков

  • один кэш-ключ может использоваться в разных компонентах
  • изменения затронут все подписки одновременно

Практика синхронизации UI без лишних запросов

Ручное обновление кэша часто используется для уменьшения сетевой нагрузки:

  • добавление элемента в список без refetch
  • локальное изменение статуса (лайк, просмотр)
  • мгновенное отображение пользовательских действий

RTK Query в этом сценарии выступает как гибрид:

  • сервер остаётся источником истины
  • кэш временно отражает предполагаемое состояние
  • синхронизация происходит асинхронно