Кеш в TanStack Query представляет собой централизованное хранилище
данных, полученных с сервера. Все результаты запросов сохраняются внутри
QueryCache, а доступ к ним осуществляется через
QueryClient.
При работе с кешем необходимо учитывать, что данные:
Безопасность кеша становится особенно важной в приложениях с:
Основная ошибка — помещение чувствительных данных в query cache.
Особенно опасно хранить:
Плохой пример:
useQuery({
queryKey: ['auth'],
queryFn: async () => {
return {
accessToken: 'jwt-token',
refreshToken: 'refresh-token',
password: '123456'
}
}
})
Даже если приложение работает локально, данные:
Безопасная архитектура начинается с разделения данных по уровню чувствительности.
| Тип данных | Где хранить |
|---|---|
| Access token | HttpOnly cookie |
| Refresh token | HttpOnly cookie |
| UI-state | React state |
| Данные API | TanStack Query |
| Форма логина | Local component state |
| Настройки интерфейса | LocalStorage |
Хранение токенов в кеше TanStack Query создаёт риск XSS-атак.
При использовании HttpOnly cookie:
Небезопасный вариант:
queryClient.setQueryData(['token'], accessToken)
Безопасный вариант:
await fetch('/api/profile', {
credentials: 'include'
})
В этом случае токен вообще не присутствует в JavaScript-коде.
После выхода пользователя необходимо полностью удалять приватные данные из кеша.
Если этого не сделать:
Правильный logout:
const logout = async () => {
await api.logout()
queryClient.clear()
}
Полностью очищает весь кеш.
queryClient.clear()
Удаляется:
Используется при logout.
Удаляет конкретные запросы.
queryClient.removeQueries({
queryKey: ['profile']
})
Подходит для удаления чувствительных данных.
Сбрасывает состояние запросов.
queryClient.resetQueries({
queryKey: ['profile']
})
Не гарантирует полного удаления данных.
Чем дольше данные живут в памяти, тем выше риск утечки.
TanStack Query предоставляет несколько механизмов управления временем хранения.
staleTime определяет, как долго данные считаются
актуальными.
useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
staleTime: 0
})
Для чувствительных данных рекомендуется минимальный
staleTime.
Особенно для:
gcTime определяет, как долго данные остаются в памяти
после потери подписчиков.
useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
gcTime: 0
})
При gcTime: 0 данные удаляются сразу после
размонтирования компонента.
Это полезно для:
Persistence позволяет сохранять кеш между перезагрузками страницы.
Обычно используется:
Но persistence крайне опасен для чувствительных данных.
LocalStorage:
Плохой пример:
persistQueryClient({
queryClient,
persister
})
Если в кеше находятся персональные данные, они окажутся в LocalStorage.
TanStack Query позволяет фильтровать запросы.
persistQueryClient({
queryClient,
persister,
dehydrateOptions: {
shouldDehydrateQuery: (query) => {
return query.queryKey[0] !== 'private'
}
}
})
Теперь запросы с ключом private не сохраняются.
Удобная стратегия — маркировать приватные query через
meta.
useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
meta: {
sensitive: true
}
})
Фильтрация:
shouldDehydrateQuery: (query) => {
return !query.meta?.sensitive
}
Такой подход особенно полезен в крупных приложениях.
Критическая проблема — повторное использование кеша между аккаунтами.
Сценарий:
Правильный подход:
useQuery({
queryKey: ['profile', userId],
queryFn: fetchProfile
})
Теперь кеш разделяется между пользователями.
Неправильно:
queryKey: ['profile']
Правильно:
queryKey: ['profile', userId]
После авторизации нового пользователя необходимо инвалидировать предыдущий кеш.
await queryClient.invalidateQueries()
Либо полностью очищать:
queryClient.clear()
React Query Devtools показывают содержимое кеша.
В production-среде Devtools необходимо отключать.
Плохо:
<ReactQueryDevtools />
Безопаснее:
{process.env.NODE_ENV === 'development' && (
<ReactQueryDevtools />
)}
При Server-Side Rendering данные могут попасть в HTML страницы.
Особенно опасно:
dehydrate() сериализует кеш.
const dehydratedState = dehydrate(queryClient)
Если внутри находятся приватные данные, они попадут:
Безопасный вариант:
dehydrate(queryClient, {
shouldDehydrateQuery: (query) => {
return !query.meta?.sensitive
}
})
Если persistence всё же необходим, данные желательно шифровать.
Например:
const encrypted = encrypt(JSON.stringify(cache))
Однако важно понимать:
Шифрование снижает риск случайной компрометации, но не решает проблему полностью.
Не следует помещать в кеш избыточные объёмы информации.
Плохо:
return {
user,
tokens,
permissions,
auditLogs,
paymentHistory,
internalSecrets
}
Лучше:
return {
id: user.id,
name: user.name,
avatar: user.avatar
}
Минимизация данных уменьшает:
Сервер не должен отправлять лишние поля.
Даже если frontend их не использует, они попадут в кеш.
Опасный API-response:
{
"id": 1,
"name": "Alex",
"passwordHash": "...",
"internalRole": "root",
"secretKey": "..."
}
Безопасный response:
{
"id": 1,
"name": "Alex"
}
TanStack Query позволяет преобразовывать ответ перед попаданием в компоненты.
useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
select: (data) => ({
id: data.id,
name: data.name
})
})
Это уменьшает объём данных, доступных UI.
Optimistic updates временно помещают данные в кеш до подтверждения сервера.
onMutate: async (newTodo) => {
queryClient.setQueryData(['todos'], old => [
...old,
newTodo
])
}
Если optimistic update содержит чувствительные данные, они:
onError: (error, variables, context) => {
queryClient.setQueryData(
['todos'],
context.previousTodos
)
}
Rollback предотвращает сохранение некорректных данных.
Mutation cache тоже хранит данные.
После завершения операции желательно удалять приватные результаты.
mutation.reset()
Либо:
queryClient.getMutationCache().clear()
Если persistence необходим временно, SessionStorage безопаснее.
Преимущества:
Но XSS-риски всё равно сохраняются.
Content Security Policy снижает вероятность XSS.
Пример:
Content-Security-Policy:
default-src 'self';
script-src 'self';
Даже идеально настроенный TanStack Query не защищает от XSS без CSP.
Trusted Types предотвращают опасные DOM-инъекции.
Особенно полезны в React-приложениях с:
В крупных системах иногда используются отдельные QueryClient-инстансы.
Пример:
const publicClient = new QueryClient()
const privateClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 0
}
}
})
Так можно:
Во время logout могут завершиться старые запросы.
Опасный сценарий:
Безопасный logout:
const logout = async () => {
await queryClient.cancelQueries()
queryClient.clear()
await api.logout()
}
TanStack Query поддерживает отмену запросов.
useQuery({
queryKey: ['profile'],
queryFn: async ({ signal }) => {
const response = await fetch('/api/profile', {
signal
})
return response.json()
}
})
Это предотвращает попадание устаревших данных в кеш.
Даже при наличии кеша сервер обязан проверять авторизацию.
Нельзя полагаться на frontend cache.
Плохо:
if (cachedUser.isAdmin) {
return sensitiveData
}
Безопасно:
const response = await fetch('/api/admin')
Сервер самостоятельно проверяет права доступа.
Безопасная архитектура строится на предположении, что:
Поэтому кеш должен содержать минимум чувствительной информации.
Надёжная схема обычно выглядит так: