Разделение кода и динамические импорты

Архитектура клиентских приложений на JavaScript напрямую зависит от того, как организована загрузка модулей. При использовании TanStack Query ключевым становится не только управление серверным состоянием, но и грамотное разделение кода, чтобы минимизировать начальный бандл и ускорить время первого рендера.

Разделение кода в связке с TanStack Query затрагивает несколько уровней: маршрутизацию, загрузку компонентов, подключение query-функций и даже инициализацию отдельных QueryClient-контекстов в специфических сценариях.


Базовый принцип: изоляция query-логики от UI

Основная ошибка в архитектуре — хранение всей логики запросов внутри компонентов, которые загружаются сразу при старте приложения. Это приводит к тому, что даже неиспользуемые пользователем экраны попадают в initial bundle.

Оптимальная структура предполагает разделение:

  • UI-компоненты
  • функции получения данных (query functions)
  • хуки TanStack Query
  • маршруты и страницы

Такой подход позволяет загружать данные и код только тогда, когда они реально требуются.

Пример разделения:

// 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().

React.lazy и маршрутизация

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 на динамическую загрузку

TanStack Query сам по себе не участвует в code splitting, но влияет на структуру импортов:

  • query hooks обычно импортируются внутри lazy-модулей
  • query functions могут быть отделены в отдельные чанки
  • prefetching может инициировать загрузку модулей заранее

Важно понимать, что сам QueryClient должен оставаться в основном бандле, так как он нужен на уровне всего приложения.


Ленивая загрузка query-функций

Иногда имеет смысл выносить даже функции запросов в динамические импорты, особенно если API разделено по доменам.

Пример динамического import внутри queryFn

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);
    },
  });
}

Такой подход позволяет:

  • уменьшить initial bundle
  • загружать API-модули только при необходимости
  • разделять доменные области приложения

Однако чрезмерное дробление может привести к росту количества HTTP-запросов за чанками.


Разделение по доменам и feature-based архитектура

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 остаются локальными, а их загрузка происходит только при активации маршрута.


Prefetch как инструмент управления загрузкой чанков

TanStack Query позволяет не только лениво загружать данные, но и заранее инициировать загрузку.

Prefetch при наведении

import { queryClient } from './queryClient';

function preloadUsers() {
  queryClient.prefetchQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const { fetchUsers } = await import('./features/users/api/fetchUsers');
      return fetchUsers();
    },
  });
}

При таком подходе происходит:

  • загрузка JS-чанка с API
  • заполнение кеша TanStack Query
  • ускорение перехода на страницу

Prefetch становится связующим звеном между code splitting и UX-оптимизацией.


Динамическая регистрация зависимостей через queryKey

TanStack Query опирается на queryKey как на идентификатор зависимости. Это позволяет строить архитектуру, где разные feature-модули могут быть изолированы.

useQuery({
  queryKey: ['users', { page: 1 }],
  queryFn: fetchUsers,
});

При ленивой загрузке feature важно, чтобы ключи оставались стабильными, иначе кеширование теряет смысл и создаются дубликаты запросов.


Lazy-loading и QueryClientProvider

QueryClientProvider должен быть загружен на верхнем уровне приложения и не участвовать в code splitting. Однако можно разделять конфигурацию клиента:

// queryClient.js
import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60,
      refetchOnWindowFocus: false,
    },
  },
});

Важно, что сам экземпляр клиента должен быть singleton, иначе кеш будет разрушен при пересоздании чанков.


SSR и динамические импорты

При серверном рендеринге code splitting требует дополнительного контроля:

  • необходимо заранее загружать чанки перед рендером
  • использовать dehydrate / hydrate из TanStack Query
  • избегать асинхронных импортов внутри queryFn на сервере без кеширования
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

Создание нового QueryClient в каждом feature приводит к:

  • потере кеша
  • повторным запросам
  • нестабильному состоянию

Импорт query hooks в глобальном scope

Если hooks импортируются в основном бандле, lazy-loading теряет смысл. Часто встречается ситуация:

import { useUsersQuery } from './features/users/queries/useUsersQuery';

в корневом компоненте — это полностью отменяет разделение кода.


Слишком мелкое дробление import()

Избыточный import() на уровне каждой функции запроса увеличивает количество чанков и ухудшает производительность загрузки.


Баланс между bundle size и latency

Эффективная стратегия строится на трёх уровнях:

  1. Грубое разделение по маршрутам
  2. Feature-based модули
  3. Точечный dynamic import только для тяжёлых API или редко используемых функций

TanStack Query в этой модели выступает как слой кеширования, который сглаживает задержки между загрузкой чанков и получением данных.


Взаимодействие с bundler-ами (Vite, Webpack)

Современные сборщики позволяют усиливать эффект code splitting:

  • Vite автоматически разделяет динамические import()
  • Webpack поддерживает magic comments для именования чанков
  • Tree-shaking убирает неиспользуемые query hooks

Пример для Webpack:

const fetchUsers = () =>
  import(
    /* webpackChunkName: "users-api" */
    './features/users/api/fetchUsers'
  );

Оптимизация загрузки через комбинирование кеша и чанков

Наиболее эффективная модель выглядит как связка:

  • динамический импорт модуля
  • мгновенное выполнение queryFn
  • сохранение результата в кеше TanStack Query

Такой подход уменьшает ощущение загрузки:

  1. модуль загружается
  2. данные сразу берутся из API или кеша
  3. повторные переходы не требуют загрузки чанка

Lazy features как единица архитектуры

В крупных приложениях feature становится минимальной единицей code splitting. Каждая feature:

  • содержит собственные query hooks
  • управляет своим набором query keys
  • может быть полностью изолирована
  • загружается через import()

TanStack Query в этом контексте становится механизмом синхронизации данных между ленивыми частями приложения, сохраняя консистентность состояния даже при асинхронной загрузке кода.