Современные веб-приложения часто открываются одновременно в нескольких вкладках браузера. Пользователь может авторизоваться в одной вкладке, изменить данные в другой, удалить запись в третьей или выполнить logout в четвёртой. Без механизма синхронизации каждая вкладка продолжает жить со своим локальным состоянием кеша.
В контексте TanStack Query это приводит к нескольким проблемам:
TanStack Query предоставляет инструменты для синхронизации состояния между вкладками и окнами браузера, позволяя поддерживать единый актуальный кеш.
Каждый экземпляр QueryClient существует только внутри
текущего JavaScript-контекста. Это означает:
QueryClient;Пример:
const queryClient = new QueryClient()
Если пользователь открыл приложение в двух вкладках:
Даже если query key совпадают, кеш физически остаётся раздельным.
Для решения этой проблемы используется механизм broadcast-коммуникации между вкладками.
TanStack Query предоставляет экспериментальный пакет:
npm install @tanstack/query-broadcast-client-experimental
Пакет позволяет:
Базовая настройка:
import { QueryClient } from '@tanstack/react-query'
import { broadcastQueryClient } from '@tanstack/query-broadcast-client-experimental'
const queryClient = new QueryClient()
broadcastQueryClient({
queryClient,
broadcastChannel: 'app-cache'
})
После подключения:
Под капотом используется браузерный API:
new BroadcastChannel('channel-name')
Он позволяет:
Пример низкоуровневой работы:
const channel = new BroadcastChannel('app')
channel.postMessage({
type: 'invalidate',
key: ['users']
})
channel.onmess age = (event) => {
console.log(event.data)
}
TanStack Query абстрагирует этот механизм.
Наиболее важный сценарий — распространение invalidation.
Пример:
queryClient.invalidateQueries({
queryKey: ['users']
})
Без broadcast:
С broadcast:
Mutation — основной источник изменений данных.
Пример:
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['users']
})
}
})
После успешного обновления:
Broadcast работает не только с invalidation, но и с прямым обновлением кеша.
Пример:
queryClient.setQueryData(
['user', user.id],
user
)
Обновление автоматически отправляется другим вкладкам.
Это особенно полезно для:
Внутри broadcast-клиент передаёт сериализованные события.
Типичные события:
{
type: 'queryUpdated',
queryHash: '["users"]',
state: {}
}
Либо:
{
type: 'queryRemoved'
}
Либо:
{
type: 'invalidateQueries'
}
Каждая вкладка подписывается на канал и обновляет локальный кеш.
BroadcastChannel работает не во всех условиях.
Основные ограничения:
Если вкладка была закрыта во время события, сообщение теряется.
Часто broadcast используется вместе с persistence-кешем.
Пакет:
npm install @tanstack/react-query-persist-client
Пример:
persistQueryClient({
queryClient,
persister
})
Комбинация даёт:
До появления BroadcastChannel синхронизация часто строилась через
localStorage.
Пример:
localStorage.setItem(
'query-sync',
JSON.stringify({
type: 'invalidate',
key: ['users']
})
)
Другие вкладки:
window.addEventListener('storage', (event) => {
console.log(event.newValue)
})
Недостатки подхода:
BroadcastChannel значительно эффективнее.
Один из самых распространённых сценариев — logout.
Пример:
const logout = async () => {
await api.logout()
queryClient.clear()
}
Без синхронизации:
С broadcast:
Пример query:
useQuery({
queryKey: ['settings'],
queryFn: fetchSettings
})
Если пользователь меняет тему интерфейса:
queryClient.setQueryData(
['settings'],
updatedSettings
)
Все вкладки моментально получают:
При ручной синхронизации через события можно случайно создать цикл:
Broadcast-клиент TanStack Query защищает от подобных повторов.
Важно понимать разницу между:
Пример:
queryClient.invalidateQueries({
queryKey: ['posts']
})
Даже если сами данные не изменились:
TanStack Query автоматически refetch-ит данные при возврате фокуса вкладки.
Пример механизма:
refetchOnWindowFocus: true
В сочетании с broadcast получается:
Optimistic updates становятся сложнее при нескольких вкладках.
Пример:
queryClient.setQueryData(
['todos'],
optimisticTodos
)
Проблемы:
Рекомендуется:
Типичная ситуация:
Возможные последствия:
TanStack Query не решает серверные конфликты автоматически. Необходимы:
Broadcast-события имеют стоимость.
Проблемы при большом количестве query:
Особенно заметно при:
Полезные практики:
Плохо:
queryClient.invalidateQueries()
Лучше:
queryClient.invalidateQueries({
queryKey: ['users']
})
Вместо глобального refetch:
queryClient.setQueryData(
['user', id],
updatedUser
)
Слишком дробный кеш создаёт:
При отладке нескольких вкладок полезно наблюдать:
Для этого используются Devtools:
npm install @tanstack/react-query-devtools
Подключение:
<ReactQueryDevtools initialIsOpen />
Devtools помогают увидеть:
При offline-first архитектуре вкладки могут иметь разные состояния сети.
Пример:
После восстановления соединения:
Здесь особенно важны:
Broadcast хорошо сочетается с WebSocket.
Сценарий:
Пример:
socket.on('userUpdated', (user) => {
queryClient.setQueryData(
['user', user.id],
user
)
})
Остальные вкладки получают обновление без собственных websocket-соединений.
В крупных приложениях иногда используется Shared Worker.
Преимущества:
Недостатки:
Для большинства приложений BroadcastChannel оказывается достаточным решением.
Типичная архитектура выглядит так:
Mutation
↓
setQueryData / invalidateQueries
↓
broadcastQueryClient
↓
BroadcastChannel
↓
Другие вкладки
↓
refetch / cache update
Такой подход обеспечивает: