В TanStack Query дедупликация запросов представляет собой механизм
предотвращения повторного выполнения одинаковых запросов к серверу при
наличии нескольких подписчиков или параллельных вызовов одного и того же
queryKey. Базовая идея заключается в том, что один сетевой
запрос может обслуживать несколько потребителей данных, если их контекст
совпадает.
Ключевая единица дедупликации — это queryHash,
который вычисляется на основе queryKey и нормализованной
структуры запроса. Если два запроса имеют одинаковый
queryHash, библиотека считает их идентичными.
Дедупликация в TanStack Query работает в рамках одного
QueryClient и распространяется на:
useQuery с одинаковым
queryKeyqueryClient.fetchQueryВажно, что дедупликация не зависит от компонента или места вызова. Она работает на уровне кэша и менеджера запросов.
При нескольких одновременных запросах с одинаковым ключом библиотека не инициирует отдельные HTTP-вызовы. Вместо этого:
queryFnПример:
import { useQuery } fr om '@tanstack/react-query'
function A() {
const query = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
return null
}
function B() {
const query = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
return null
}
Если компоненты A и B смонтированы
одновременно, fetchUsers выполнится только один раз.
Когда инициируется запрос, TanStack Query создаёт или переиспользует
объект Query, связанный с конкретным
queryHash. Внутри него хранится состояние:
promise — текущий выполняющийся запросobservers — список подписчиковstate — данные, ошибки, статус загрузкиЕсли запрос уже находится в состоянии fetching, новые
подписчики не создают новый queryFn вызов, а присоединяются
к существующему promise.
Упрощённая модель:
if (query.isFetching) {
return query.promise
} else {
query.promise = queryFn()
return query.promise
}
Дедупликация работает не только в React-хуках, но и на уровне клиента:
await queryClient.fetchQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
})
Если в момент вызова уже выполняется запрос с таким же ключом, второй вызов не создаёт новый HTTP-запрос, а ожидает результат первого.
То же поведение применимо к:
prefetchQueryensureQueryDataЗапрос считается активным, пока у него есть подписчики. Подписчики появляются при:
useQueryqueryClient.getQueryCache().subscribeКогда последний подписчик отписывается, запрос переходит в состояние
inactive, но результат остаётся в кэше до истечения
gcTime.
Дедупликация тесно связана с этим механизмом: пока запрос активен, повторный запуск не создаётся.
Дедупликация и кэширование — разные уровни оптимизации.
Дедупликация:
Кэширование:
staleTime и gcTimeПример:
useQuery({
queryKey: ['user', 1],
queryFn: fetchUser,
staleTime: 5000,
})
Если два компонента одновременно монтируются, сработает дедупликация. Если второй компонент смонтируется через минуту — запрос может не выполняться вовсе из-за кэша.
staleTime не влияет напрямую на дедупликацию, но влияет
на необходимость запроса.
staleTime: Infinity запрос не считается
устаревшимОднако при одновременном запуске нескольких запросов даже с
staleTime: Infinity, если данных нет в кэше, дедупликация
всё равно активируется.
При refetch дедупликация работает только если запрос уже
выполняется.
queryClient.invalidateQueries({ queryKey: ['todos'] })
Если несколько компонентов одновременно вызывают invalidation, TanStack Query объединяет рефетчи:
Но если предыдущий запрос завершён, новый refetch создаёт отдельный вызов.
Любая дедупликация строится на нормализованном queryKey.
Примеры:
['users']
['users', { page: 1 }]
['users', { page: 2 }]
Эти ключи считаются разными запросами.
Однако важно учитывать, что порядок и структура объекта влияет на hash:
['users', { page: 1, lim it: 10 }]
['users', { limit: 10, page: 1 }]
Внутри TanStack Query происходит нормализация, поэтому такие ключи считаются одинаковыми.
Дедупликация не зависит от queryFn. Даже если функции
разные по ссылке, но queryKey одинаковый, результат будет
один.
useQuery({
queryKey: ['data'],
queryFn: () => fetch('/api/a'),
})
useQuery({
queryKey: ['data'],
queryFn: () => fetch('/api/b'),
})
Фактически будет использоваться один из queryFn (тот,
который был зарегистрирован первым для данного key), поэтому такая
конфигурация считается ошибочной и может приводить к неопределённому
поведению.
Если дедуплицированный запрос завершился ошибкой:
errorretryВажно, что повторный retry также дедуплицируется, если он инициирован одновременно несколькими подписчиками.
При включённом retry:
useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
retry: 3,
})
Если несколько компонентов инициируют запрос одновременно, все retry-итерации объединяются. Это предотвращает ситуацию, когда каждый подписчик отдельно повторяет запросы.
Параметр networkMode может изменять поведение
дедупликации:
online — стандартное поведениеalways — запросы выполняются даже при оффлайнеofflineFirst — может менять стратегию получения
данныхОднако сама дедупликация по queryKey сохраняется во всех
режимах.
Если компонент размонтируется и монтируется снова, но запрос ещё активен:
useQuery присоединяется к текущему запросуПри SPA-навигации несколько страниц могут запрашивать одинаковые данные:
При гидратации состояния дедупликация может происходить уже после восстановления кэша, предотвращая повторный запрос на клиенте.
Дедупликация снижает:
Особенно заметно в приложениях с:
Несмотря на эффективность, дедупликация имеет ограничения:
QueryClientqueryKeyqueryFn