RTK Query предоставляет встроенные механизмы для управления кэшем, но его архитектура также позволяет организовывать реалтайм-обновления данных без отказа от преимуществ декларативного API. Реалтайм в контексте RTK Query означает синхронизацию состояния кэша с внешними событиями: WebSocket-сообщениями, SSE-стримами, polling-механизмами и внешними диспетчеризуемыми событиями Redux.
Основная сложность реалтайм-обновлений заключается не в получении
данных, а в их корректной интеграции в уже существующий кэш RTK Query
без нарушения инвалидации, дедупликации и подписочной модели. RTK Query
решает это через updateQueryData,
invalidateTags, onCacheEntryAdded и прямую
работу с Redux-экшенами, которые изменяют state cache slice.
Кэш RTK Query представляет собой нормализованную структуру, где каждый endpoint хранит результаты запросов, индексированные по аргументам. Каждый запрос имеет:
Любое реалтайм-обновление должно учитывать три аспекта:
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
безопасно и иммутабельно.
Ключевые особенности:
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Критически важно, что обновления происходят только если запрос активен. Это предотвращает утечки памяти и лишнюю обработку событий.
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 автоматически:
Этот подход не является истинно реалтайм-обновлением, но часто используется как его упрощённая форма.
На практике чаще используется комбинация:
updateCachedData для мгновенного отражения
измененийinvalidateTags для периодической синхронизацииПример сценария:
updateCachedDatainvalidateTags для сверки с
серверомРеалтайм-данные могут конфликтовать с локальными изменениями или параллельными запросами. RTK Query не решает конфликт автоматически, поэтому используются стратегии:
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();
}
}
});
При WebSocket обновлениях:
updateCachedData((draft) => {
const index = draft.findIndex(m => m.id === message.id);
if (index !== -1) {
draft[index] = message;
} else {
draft.push(message);
}
});
Если сервер передаёт version или updatedAt,
можно игнорировать устаревшие события:
if (incoming.updatedAt > existing.updatedAt) {
draft[index] = incoming;
}
RTK Query поддерживает polling через
refetchOnMountOrArgChange и
pollingInterval.
useGetMessagesQuery(chatId, {
pollingInterval: 5000
});
Особенности:
Гибридная модель:
Это особенно полезно в нестабильных сетях, где WebSocket может терять соединение.
Даже при использовании WebSocket или SSE, invalidateTags
часто используется как fallback:
socket.oncl ose = () => {
store.dispatch(
api.util.invalidateTags([{ type: 'Messages', id: chatId }])
);
};
RTK Query автоматически управляет жизненным циклом cache entry:
Реалтайм-слушатели должны привязываться именно к этому жизненному циклу, иначе возникает:
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);
}
});
RTK Query минимизирует перерендеры за счёт:
Реалтайм-обновления сохраняют эти свойства при условии:
updateCachedDataИногда реалтайм-логика выносится в 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 предоставляет инструменты, но не навязывает архитектуру реалтайма, оставляя её на уровне интеграции Redux и внешних источников событий.