Реалтайм обновления кэша

RTK Query предоставляет встроенные механизмы для управления кэшем, но его архитектура также позволяет организовывать реалтайм-обновления данных без отказа от преимуществ декларативного API. Реалтайм в контексте RTK Query означает синхронизацию состояния кэша с внешними событиями: WebSocket-сообщениями, SSE-стримами, polling-механизмами и внешними диспетчеризуемыми событиями Redux.

Основная сложность реалтайм-обновлений заключается не в получении данных, а в их корректной интеграции в уже существующий кэш RTK Query без нарушения инвалидации, дедупликации и подписочной модели. RTK Query решает это через updateQueryData, invalidateTags, onCacheEntryAdded и прямую работу с Redux-экшенами, которые изменяют state cache slice.


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

  • ключ кэша (endpoint + args)
  • подписчиков (components, hooks)
  • жизненный цикл (fetching, fulfilled, removed)

Любое реалтайм-обновление должно учитывать три аспекта:

  1. Сохранение согласованности между всеми подписчиками
  2. Обновление только релевантных записей
  3. Минимизация лишних перерендеров

Использование updateQueryData для локальных обновлений

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

import { api } from './api';

store.dispatch(
  api.util.updateQueryData('getMessages', { chatId: 1 }, (draft) => {
    draft.push({
      id: 101,
      text: 'Новое сообщение',
      createdAt: Date.now()
    });
  })
);

Внутри используется Immer, поэтому изменение draft безопасно и иммутабельно.

Ключевые особенности:

  • обновление происходит синхронно
  • все активные подписчики получают обновление
  • повторный fetch не требуется
  • обновляется только конкретный cache entry

Интеграция WebSocket через onCacheEntryAdded

Для реалтайм сценариев RTK Query предоставляет onCacheEntryAdded, который запускается при создании cache entry и позволяет подключить внешние источники данных.

getMessages: builder.query({
  query: (chatId) => `/messages?chatId=${chatId}`,

  async onCacheEntryAdded(
    chatId,
    { updateCachedData, cacheDataLoaded, cacheEntryRemoved }
  ) {
    await cacheDataLoaded;

    const socket = new WebSocket('wss://example.com/ws');

    socket.onmess age = (event) => {
      const message = JSON.parse(event.data);

      if (message.chatId === chatId) {
        updateCachedData((draft) => {
          draft.push(message);
        });
      }
    };

    await cacheEntryRemoved;
    socket.close();
  }
});

Поведение updateCachedData внутри подписки

updateCachedData является локальной версией updateQueryData, привязанной к конкретному cache entry. Она:

  • доступна только внутри onCacheEntryAdded
  • автоматически синхронизирует все подписки
  • завершает обновления при удалении cache entry

Критически важно, что обновления происходят только если запрос активен. Это предотвращает утечки памяти и лишнюю обработку событий.


Реалтайм через invalidateTags и автоматический refetch

Другой подход к обновлению данных — использование тегов.

getMessages: builder.query({
  query: (chatId) => `/messages?chatId=${chatId}`,
  providesTags: (result, error, chatId) => [
    { type: 'Messages', id: chatId }
  ]
});

addMessage: builder.mutation({
  query: (payload) => ({
    url: '/messages',
    method: 'POST',
    body: payload
  }),
  invalidatesTags: (result, error, arg) => [
    { type: 'Messages', id: arg.chatId }
  ]
});

После выполнения mutation RTK Query автоматически:

  • помечает cache entry как устаревший
  • запускает refetch активных запросов
  • синхронизирует все подписчики

Этот подход не является истинно реалтайм-обновлением, но часто используется как его упрощённая форма.


Гибридная модель: WebSocket + локальный кэш

На практике чаще используется комбинация:

  • WebSocket для событий
  • updateCachedData для мгновенного отражения изменений
  • invalidateTags для периодической синхронизации

Пример сценария:

  1. приходит событие “messageCreated”
  2. данные вставляются в кэш через updateCachedData
  3. раз в N минут выполняется invalidateTags для сверки с сервером

Обработка конфликтов состояния

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

1. Optimistic upd ate + rollback

addMessage: builder.mutation({
  async onQueryStarted(arg, { dispatch, queryFulfilled }) {
    const patch = dispatch(
      api.util.updateQueryData('getMessages', arg.chatId, (draft) => {
        draft.push({ ...arg, temp: true });
      })
    );

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

2. Last-write-wins стратегия

При WebSocket обновлениях:

updateCachedData((draft) => {
  const index = draft.findIndex(m => m.id === message.id);

  if (index !== -1) {
    draft[index] = message;
  } else {
    draft.push(message);
  }
});

3. Версионность данных

Если сервер передаёт version или updatedAt, можно игнорировать устаревшие события:

if (incoming.updatedAt > existing.updatedAt) {
  draft[index] = incoming;
}

Polling как псевдо-реалтайм

RTK Query поддерживает polling через refetchOnMountOrArgChange и pollingInterval.

useGetMessagesQuery(chatId, {
  pollingInterval: 5000
});

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

  • простая реализация
  • предсказуемая нагрузка
  • отсутствие WebSocket зависимости
  • но задержка обновления фиксированная

Комбинация polling и событий

Гибридная модель:

  • WebSocket даёт мгновенные изменения
  • polling корректирует расхождения
  • RTK Query кэш объединяет источники

Это особенно полезно в нестабильных сетях, где WebSocket может терять соединение.


Инвалидация как резервный механизм

Даже при использовании WebSocket или SSE, invalidateTags часто используется как fallback:

  • при reconnect WebSocket
  • при потере событий
  • при критических изменениях данных
socket.oncl ose = () => {
  store.dispatch(
    api.util.invalidateTags([{ type: 'Messages', id: chatId }])
  );
};

Управление жизненным циклом реалтайм-данных

RTK Query автоматически управляет жизненным циклом cache entry:

  • создание при первом подписчике
  • удержание при активных подписках
  • удаление после unsubscribe

Реалтайм-слушатели должны привязываться именно к этому жизненному циклу, иначе возникает:

  • утечка WebSocket соединений
  • дублирование событий
  • неконсистентный state

onCacheEntryAdded является единственным корректным способом привязки.


Оптимизация частоты обновлений

При высокочастотных событиях (например, тикеры или чаты с массовыми сообщениями) необходимо снижать нагрузку:

Батчинг обновлений

let buffer = [];

socket.onmess age = (event) => {
  buffer.push(JSON.parse(event.data));
};

setInterval(() => {
  if (buffer.length) {
    updateCachedData((draft) => {
      draft.push(...buffer);
    });
    buffer = [];
  }
}, 200);

Дедупликация событий

const seen = new Se t();

updateCachedData((draft) => {
  if (!seen.has(message.id)) {
    draft.push(message);
    seen.add(message.id);
  }
});

Взаимодействие с React и перерендеры

RTK Query минимизирует перерендеры за счёт:

  • структурного sharing ссылок
  • селективной подписки
  • мемоизации результата query hooks

Реалтайм-обновления сохраняют эти свойства при условии:

  • изменения только внутри updateCachedData
  • отсутствия пересоздания объектов вне Immer draft
  • стабильных селекторов

Централизованный event bus поверх RTK Query

Иногда реалтайм-логика выносится в middleware:

const realtimeMiddleware = (store) => (next) => (action) => {
  if (action.type === 'ws/message') {
    store.dispatch(
      api.util.updateQueryData('getMessages', action.payload.chatId, (draft) => {
        draft.push(action.payload);
      })
    );
  }

  return next(action);
};

Такой подход полезен, когда WebSocket используется глобально, а не на уровне endpoint.


Ограничения модели RTK Query в реалтайме

Несмотря на гибкость, существуют ограничения:

  • нет встроенного reconcilation engine
  • нет автоматического conflict resolution
  • WebSocket слой полностью пользовательский
  • сложные графы зависимостей требуют ручного контроля

RTK Query предоставляет инструменты, но не навязывает архитектуру реалтайма, оставляя её на уровне интеграции Redux и внешних источников событий.