Одной из базовых стратегий является изоляция логики запросов по функциональным областям. Вместо централизованного файла с query-функциями используется модульная структура, где каждый домен (users, orders, auth, products) содержит собственные хуки и ключи запросов.
// features/users/queries.js
export const userKeys = {
all: ['users'],
detail: (id) => ['users', 'detail', id],
list: (params) => ['users', 'list', params],
};
export async function fetchUser(id) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
// features/users/useUser.js
import { useQuery } from '@tanstack/react-query';
import { fetchUser, userKeys } from './queries';
export function useUser(id) {
return useQuery({
queryKey: userKeys.detail(id),
queryFn: () => fetchUser(id),
enabled: !!id,
});
}
Такой подход позволяет переносить целые фичи в отдельные чанки, если используется route-based или feature-based splitting.
При использовании React Router или аналогичных систем маршрутизации логика TanStack Query может быть привязана к маршрутам и загружаться только при переходе на страницу.
import { lazy } from 'react';
const UsersPage = lazy(() => import('./pages/UsersPage'));
const OrdersPage = lazy(() => import('./pages/OrdersPage'));
Внутри страницы находится изолированный набор хуков:
// pages/UsersPage.jsx
import { useUsers } from '../features/users/useUsers';
export default function UsersPage() {
const { data, isLoading } = useUsers();
if (isLoading) return 'loading...';
return data.map(u => <div key={u.id}>{u.name}</div>);
}
Ключевая идея: TanStack Query не требует глобальной регистрации запросов, поэтому их можно полностью переносить в lazy-loaded модули без потери функциональности.
Более агрессивная стратегия — динамический импорт самих query-функций или даже хуков.
import { useQuery } from '@tanstack/react-query';
export function useDynamicProduct(id) {
return useQuery({
queryKey: ['product', id],
queryFn: async () => {
const { fetchProduct } = await import('./api/fetchProduct');
return fetchProduct(id);
},
});
}
Преимущество заключается в том, что код API-слоя попадает в отдельный chunk и не загружается до момента фактического использования запроса.
В крупных приложениях возможна сегментация QueryClient на уровне поддеревьев React. Это позволяет отделять кэши и политики повторного использования данных.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const adminQueryClient = new QueryClient();
const publicQueryClient = new QueryClient();
function AdminApp() {
return (
<QueryClientProvider client={adminQueryClient}>
<AdminRoutes />
</QueryClientProvider>
);
}
function PublicApp() {
return (
<QueryClientProvider client={publicQueryClient}>
<PublicRoutes />
</QueryClientProvider>
);
}
Такой подход усиливает code splitting на уровне runtime-изоляции, а не только бандлинга.
Эффективная архитектура достигается через явное разделение слоёв:
// api/orders.js
export const getOrders = async () => {
const res = await fetch('/api/orders');
return res.json();
};
// queries/ordersKeys.js
export const ordersKeys = {
all: ['orders'],
};
// hooks/useOrders.js
import { useQuery } from '@tanstack/react-query';
import { getOrders } from '../api/orders';
import { ordersKeys } from '../queries/ordersKeys';
export function useOrders() {
return useQuery({
queryKey: ordersKeys.all,
queryFn: getOrders,
});
}
Каждый слой может быть вынесен в отдельный chunk, особенно при использовании barrel-free импортов.
Баррельные файлы (index.js с re-export) часто ухудшают
tree-shaking и приводят к тому, что весь feature попадает в основной
bundle.
Проблемный вариант:
// features/users/index.js
export * from './useUser';
export * from './useUsers';
Правильнее:
import { useUser } from './features/users/useUser';
Это усиливает эффективность code splitting, особенно в Vite, Webpack и esbuild.
Разделение запросов на уровне компонентов позволяет минимизировать раннюю загрузку данных.
function UserCard({ id }) {
const { data } = useUser(id);
return <div>{data?.name}</div>;
}
function UsersList({ ids }) {
return ids.map(id => <UserCard key={id} id={id} />);
}
Каждый компонент становится независимой единицей загрузки данных, особенно если используется lazy-rendering списков.
Code splitting в TanStack Query часто комбинируется с предзагрузкой, но важно разделять ответственность: preload не должен тянуть весь feature bundle.
const prefetchUser = (queryClient, id) => {
queryClient.prefetchQuery({
queryKey: ['users', id],
queryFn: async () => {
const { fetchUser } = await import('./api/fetchUser');
return fetchUser(id);
},
});
};
Таким образом, предзагрузка данных не приводит к преждевременной загрузке UI-логики.
Feature flags позволяют полностью исключать query-модули из бандла.
if (flags.enableAdvancedSearch) {
import('./features/search/advancedQueries').then(mod => {
mod.initAdvancedSearch(queryClient);
});
}
Такая схема полезна для экспериментальных фич и A/B тестов, где запросы сами по себе являются тяжёлой частью функционала.
В архитектуре микрофронтендов каждый модуль может иметь собственный QueryClient и собственный набор запросов.
// shell app
<QueryClientProvider client={sharedClient}>
<RemoteApp />
</QueryClientProvider>
или полностью изолированный вариант:
// remote app
const client = new QueryClient();
<QueryClientProvider client={client}>
<RemoteRoot />
</QueryClientProvider>
Это позволяет каждому микрофронтенду иметь собственный cache boundary и независимые code split chunks.
При серверном рендеринге важно разделять:
export async function getServerSideProps() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
}
Клиентская часть может быть лениво загружена, не затрагивая серверный код запросов.
Фабрики query позволяют переносить генерацию ключей и функций в отдельные модули, которые могут быть динамически импортированы.
export const createUserQueries = () => ({
detail: (id) => ({
queryKey: ['user', id],
queryFn: async () => {
const { fetchUser } = await import('../api/fetchUser');
return fetchUser(id);
},
}),
});
Это даёт максимальную гибкость для chunking на уровне бизнес-логики.
Code splitting можно строить не только по фичам, но и по важности данных:
export function useAnalytics() {
return useQuery({
queryKey: ['analytics'],
queryFn: async () => {
const { fetchAnalytics } = await import('./api/analytics');
return fetchAnalytics();
},
staleTime: 1000 * 60 * 10,
});
}
Некритичные запросы полностью выносятся в отдельные lazy chunks.
Инвалидация кеша часто воспринимается как глобальная операция, но её тоже можно сегментировать.
queryClient.invalidateQueries({
queryKey: ['users'],
exact: false,
});
Если домены разделены, то invalidation-логика может быть локализована в feature-модулях, что уменьшает связность между chunks.
Некоторые queryFn содержат побочные эффекты: логирование, трекинг, метрики. Эти слои можно выносить отдельно и загружать лениво.
export async function fetchUserWithMetrics(id) {
const { trackRequest } = await import('../analytics/track');
trackRequest('user_fetch');
const res = await fetch(`/api/users/${id}`);
return res.json();
}
Таким образом, аналитический слой не попадает в основной bundle.
На практике code splitting в TanStack Query строится не на одной технике, а на комбинации:
Эти механизмы усиливают друг друга и позволяют строить архитектуру, где запросы данных становятся частью управляемой системы загрузки кода, а не монолитной зависимостью.