Последовательная загрузка данных

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

Использование loader для маршрутов

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

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

const userRoute = new Route({
  path: '/user/:userId',
  loader: async ({ params }) => {
    const response = await fetch(`/api/users/${params.userId}`);
    if (!response.ok) throw new Error('Failed to fetch user');
    return response.json();
  },
});

Ключевой момент: loader выполняется до того, как компонент будет смонтирован, что обеспечивает наличие всех необходимых данных при рендеринге.

Последовательное выполнение загрузок

Для сложных маршрутов, где один маршрут зависит от данных другого, TanStack Router поддерживает вложенные маршруты с их собственными loader. Вложенные маршруты загружают данные в иерархическом порядке:

  1. Сначала вызывается loader родительского маршрута.
  2. После успешного выполнения загружается loader дочернего маршрута.

Это позволяет гарантировать, что данные родителя будут доступны дочернему маршруту.

const postsRoute = new Route({
  path: '/users/:userId/posts',
  loader: async ({ parentData }) => {
    const user = parentData.user;
    const response = await fetch(`/api/users/${user.id}/posts`);
    return response.json();
  },
});

Здесь parentData предоставляет результаты загрузки родительского маршрута (userRoute), что позволяет строить цепочки данных без дополнительного состояния.

Работа с асинхронными ошибками

TanStack Router поддерживает обработку ошибок на уровне маршрута через errorElement и блоки try/catch внутри loader. При последовательной загрузке важно корректно обрабатывать ошибки, чтобы последующие загрузки не выполнялись при сбое предыдущих:

const userRoute = new Route({
  path: '/user/:userId',
  loader: async ({ params }) => {
    try {
      const response = await fetch(`/api/users/${params.userId}`);
      if (!response.ok) throw new Error('User not found');
      return response.json();
    } catch (error) {
      throw new Error('Ошибка загрузки данных пользователя');
    }
  },
  errorElement: <UserErrorComponent />,
});

Это позволяет маршруту прерывать цепочку загрузки и отображать пользовательский компонент ошибки.

Использование defer для ленивой загрузки

Для больших объёмов данных можно комбинировать последовательную загрузку с ленивой, используя defer. defer позволяет вернуть объект с промисами, которые будут разрешены после рендеринга компонента:

const dashboardRoute = new Route({
  path: '/dashboard',
  loader: async () => {
    return {
      stats: fetch('/api/stats').then(res => res.json()),
      notifications: fetch('/api/notifications').then(res => res.json()),
    };
  },
  element: <Dashboard />,
});

Данные внутри defer могут обрабатываться асинхронно, но последовательная логика родительских loader сохраняется. Это позволяет оптимизировать время рендеринга и снизить блокировку интерфейса.

Состояние загрузки и useLoaderData

Компоненты маршрутов получают данные через хук useLoaderData, который предоставляет результат выполнения loader. При последовательной загрузке важно использовать данные родителя для инициализации дочерних компонентов:

import { useLoaderData } from '@tanstack/router';

function Posts() {
  const { posts, user } = useLoaderData();
  return (
    <div>
      <h2>Посты пользователя {user.name}</h2>
      <ul>
        {posts.map(post => <li key={post.id}>{post.title}</li>)}
      </ul>
    </div>
  );
}

Практические рекомендации

  • Иерархическая структура маршрутов: строить вложенные маршруты, чтобы естественно организовать последовательность загрузки данных.
  • Обработка ошибок на каждом уровне: предотвращает некорректное состояние интерфейса при сбое одного из loader.
  • Использование parentData: обеспечивает безопасный доступ к данным родителя, избегая повторных запросов.
  • Комбинация с defer: ускоряет рендеринг интерфейса, когда часть данных может загружаться параллельно.
  • Минимизация побочных эффектов в loader: следует избегать мутации состояния или вызовов setState внутри loader, так как они могут привести к расхождению данных и рендеров.

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