Архитектура клиентских приложений на JavaScript напрямую зависит от того, как организована загрузка модулей. При использовании TanStack Query ключевым становится не только управление серверным состоянием, но и грамотное разделение кода, чтобы минимизировать начальный бандл и ускорить время первого рендера.
Разделение кода в связке с TanStack Query затрагивает несколько
уровней: маршрутизацию, загрузку компонентов, подключение query-функций
и даже инициализацию отдельных QueryClient-контекстов в
специфических сценариях.
Основная ошибка в архитектуре — хранение всей логики запросов внутри компонентов, которые загружаются сразу при старте приложения. Это приводит к тому, что даже неиспользуемые пользователем экраны попадают в initial bundle.
Оптимальная структура предполагает разделение:
Такой подход позволяет загружать данные и код только тогда, когда они реально требуются.
Пример разделения:
// api/users.js
export async function fetchUsers() {
const res = await fetch('/api/users');
return res.json();
}
// queries/useUsersQuery.js
import { useQuery } from '@tanstack/react-query';
import { fetchUsers } from '../api/users';
export function useUsersQuery() {
return useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
}
// pages/UsersPage.jsx
import { useUsersQuery } from '../queries/useUsersQuery';
export function UsersPage() {
const { data } = useUsersQuery();
return <div>{JSON.stringify(data)}</div>;
}
Даже такая простая декомпозиция становится основой для дальнейшего code splitting.
Наиболее эффективный способ разделения кода — ленивые маршруты.
TanStack Query не накладывает ограничений на загрузку компонентов,
поэтому используется стандартный механизм import().
import { lazy, Suspense } from 'react';
const UsersPage = lazy(() => import('./pages/UsersPage'));
const PostsPage = lazy(() => import('./pages/PostsPage'));
export function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
{/* router logic */}
</Suspense>
);
}
Каждый маршрут становится отдельным чанком, и его зависимости (включая query hooks) загружаются только при переходе.
TanStack Query сам по себе не участвует в code splitting, но влияет на структуру импортов:
Важно понимать, что сам QueryClient должен оставаться в
основном бандле, так как он нужен на уровне всего приложения.
Иногда имеет смысл выносить даже функции запросов в динамические импорты, особенно если API разделено по доменам.
import { useQuery } from '@tanstack/react-query';
export function useUserQuery(userId) {
return useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const { fetchUser } = await import('../api/userApi');
return fetchUser(userId);
},
});
}
Такой подход позволяет:
Однако чрезмерное дробление может привести к росту количества HTTP-запросов за чанками.
TanStack Query хорошо сочетается с feature-based структурой:
features/
users/
api/
queries/
components/
pages/
posts/
api/
queries/
components/
pages/
Каждый feature может быть отдельным чанком:
const UsersFeature = lazy(() => import('./features/users/pages/UsersPage'));
Внутри такого feature все query hooks остаются локальными, а их загрузка происходит только при активации маршрута.
TanStack Query позволяет не только лениво загружать данные, но и заранее инициировать загрузку.
import { queryClient } from './queryClient';
function preloadUsers() {
queryClient.prefetchQuery({
queryKey: ['users'],
queryFn: async () => {
const { fetchUsers } = await import('./features/users/api/fetchUsers');
return fetchUsers();
},
});
}
При таком подходе происходит:
Prefetch становится связующим звеном между code splitting и UX-оптимизацией.
TanStack Query опирается на queryKey как на
идентификатор зависимости. Это позволяет строить архитектуру, где разные
feature-модули могут быть изолированы.
useQuery({
queryKey: ['users', { page: 1 }],
queryFn: fetchUsers,
});
При ленивой загрузке feature важно, чтобы ключи оставались стабильными, иначе кеширование теряет смысл и создаются дубликаты запросов.
QueryClientProvider должен быть загружен на верхнем
уровне приложения и не участвовать в code splitting. Однако можно
разделять конфигурацию клиента:
// queryClient.js
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60,
refetchOnWindowFocus: false,
},
},
});
Важно, что сам экземпляр клиента должен быть singleton, иначе кеш будет разрушен при пересоздании чанков.
При серверном рендеринге code splitting требует дополнительного контроля:
dehydrate / hydrate из
TanStack QueryqueryFn на сервере
без кешированияimport { dehydrate, QueryClient } from '@tanstack/react-query';
export async function loader() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return dehydrate(queryClient);
}
Создание нового QueryClient в каждом feature приводит
к:
Если hooks импортируются в основном бандле, lazy-loading теряет смысл. Часто встречается ситуация:
import { useUsersQuery } from './features/users/queries/useUsersQuery';
в корневом компоненте — это полностью отменяет разделение кода.
Избыточный import() на уровне каждой функции запроса
увеличивает количество чанков и ухудшает производительность
загрузки.
Эффективная стратегия строится на трёх уровнях:
TanStack Query в этой модели выступает как слой кеширования, который сглаживает задержки между загрузкой чанков и получением данных.
Современные сборщики позволяют усиливать эффект code splitting:
import()magic comments для именования
чанковПример для Webpack:
const fetchUsers = () =>
import(
/* webpackChunkName: "users-api" */
'./features/users/api/fetchUsers'
);
Наиболее эффективная модель выглядит как связка:
Такой подход уменьшает ощущение загрузки:
В крупных приложениях feature становится минимальной единицей code splitting. Каждая feature:
import()TanStack Query в этом контексте становится механизмом синхронизации данных между ленивыми частями приложения, сохраняя консистентность состояния даже при асинхронной загрузке кода.