Оптимизация loader'ов

Loader в TanStack Router — это асинхронная функция, которая выполняется перед рендерингом компонента маршрута. Она используется для загрузки данных, необходимых для отображения страницы, и может возвращать любые значения, которые будут доступны в компоненте через хук useLoaderData. Основная цель loader’ов — централизованное управление данными маршрутов и предотвращение состояния “loading” в самих компонентах.

Пример базового loader’а:

import { createRouter, Route } from '@tanstack/router'

const userLoader = async ({ params }) => {
  const response = await fetch(`/api/users/${params.userId}`)
  if (!response.ok) throw new Error('User not found')
  return response.json()
}

const router = createRouter({
  routes: [
    new Route({
      path: '/users/:userId',
      component: UserComponent,
      loader: userLoader,
    }),
  ],
})

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


Принципы оптимизации loader’ов

1. Кэширование данных

Асинхронные запросы в loader’ах могут быть дорогими по времени и нагрузке на сервер. Для уменьшения количества запросов используется кэширование:

  • Внутреннее кэширование: TanStack Router поддерживает кэширование значений loader’ов через свой встроенный механизм useLoaderData. Можно хранить загруженные данные в глобальном состоянии и проверять наличие данных перед выполнением запроса.
const userLoader = async ({ params, context }) => {
  if (context.userCache[params.userId]) {
    return context.userCache[params.userId]
  }
  const response = await fetch(`/api/users/${params.userId}`)
  const data = await response.json()
  context.userCache[params.userId] = data
  return data
}
  • Стороннее кэширование: Использование библиотек вроде React Query для автоматического кэширования и повторного использования запросов.

2. Ленивая загрузка данных (Lazy Loading)

Для больших страниц с множеством данных можно разделить loader на несколько частей:

  • Основной loader: загружает критические данные, необходимые для первого рендера.
  • Вспомогательные loader’ы: загружают данные для второстепенных компонентов только при их отображении.

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

const mainLoader = async ({ params }) => {
  return fetch(`/api/posts/${params.postId}`).then(res => res.json())
}

const commentsLoader = async ({ params }) => {
  return fetch(`/api/posts/${params.postId}/comments`).then(res => res.json())
}

// mainLoader вызывается сразу, commentsLoader только при рендере компонента комментариев

3. Параллельная загрузка данных

Если компонент требует несколько независимых источников данных, их лучше загружать параллельно через Promise.all, чтобы сократить общее время ожидания:

const dashboardLoader = async () => {
  const [users, posts, notifications] = await Promise.all([
    fetch('/api/users').then(res => res.json()),
    fetch('/api/posts').then(res => res.json()),
    fetch('/api/notifications').then(res => res.json())
  ])
  return { users, posts, notifications }
}

4. Дедупликация запросов

При переходе между маршрутами, использующими одинаковые loader’ы, можно избежать повторного запроса:

  • Сравнивать параметры маршрута с предыдущими.
  • Использовать глобальный кэш или state менеджеры (Redux, Zustand, React Query).
const deduplicatedLoader = async ({ params, context }) => {
  const cacheKey = `user-${params.userId}`
  if (context.cache[cacheKey]) return context.cache[cacheKey]
  const data = await fetch(`/api/users/${params.userId}`).then(res => res.json())
  context.cache[cacheKey] = data
  return data
}

5. Обработка ошибок и fallback

Loader может выбрасывать ошибки, которые затем обрабатываются через ErrorBoundary. Для оптимизации полезно:

  • Предварительно проверять кэш перед запросом.
  • Возвращать fallback-значения для временной отрисовки UI.
  • Разделять критические и некритические ошибки, чтобы некритичные не блокировали весь компонент.

6. Интеграция с Suspense

Loader’ы TanStack Router могут работать с React Suspense для отображения индикаторов загрузки:

<Suspense fallback={<Loading />}>
  <RouteComponent />
</Suspense>

Оптимизация заключается в минимизации времени ожидания критических данных и переносе второстепенной информации в отдельные Suspense boundaries.


7. Использование контекста и инъекций зависимостей

Для повторного использования loader’ов и передачи общих данных используют контексты:

const DataContext = React.createContext({})

const contextLoader = async ({ context }) => {
  if (!context.apiClient) context.apiClient = new ApiClient()
  return context.apiClient.getData()
}

Такой подход снижает количество глобальных зависимостей и улучшает повторное использование loader’ов.


8. Лимитирование количества запросов

При работе с списками данных с пагинацией полезно ограничивать количество параллельных запросов, используя throttling или batching. Это уменьшает нагрузку на API и ускоряет реакцию интерфейса.

const batchedLoader = async ({ params }) => {
  const batch = params.ids.slice(0, 10) // берем только первые 10
  return Promise.all(batch.map(id => fetch(`/api/item/${id}`).then(res => res.json())))
}

Выводы по оптимизации loader’ов

  • Использование кэширования и дедупликации уменьшает количество лишних запросов.
  • Разделение loader’ов на критические и второстепенные позволяет быстрее рендерить UI.
  • Параллельная загрузка и batching данных сокращают время ожидания.
  • Интеграция с Suspense и обработка ошибок повышает стабильность и отзывчивость интерфейса.
  • Контексты и инъекции зависимостей упрощают повторное использование loader’ов и управление состоянием.

Эти методы в комбинации делают работу loader’ов TanStack Router эффективной даже для больших, сложных приложений с множеством асинхронных зависимостей.