Управление pending-состоянием

В TanStack Router pending-состояние отражает процесс ожидания завершения асинхронных операций, связанных с навигацией. Оно используется для контроля загрузки данных, отображения индикаторов загрузки и предотвращения преждевременного отображения контента. Правильное управление этим состоянием позволяет создавать плавные пользовательские интерфейсы без “мерцания” или неконсистентных данных.


Основные механизмы pending-состояния

В TanStack Router управление pending реализуется через следующие ключевые концепции:

  1. Загрузчики (loader) Каждому маршруту можно присвоить функцию загрузки данных, которая возвращает Promise. Пока Promise не выполнен, маршрут считается в состоянии pending.

    import { createRouter, createRouteConfig } from '@tanstack/router';
    
    const route = createRouteConfig()
      .path('/users/:id')
      .loader(async ({ params }) => {
        const response = await fetch(`/api/users/${params.id}`);
        return response.json();
      });

    Здесь loader автоматически инициирует pending-состояние при вызове и завершает его после выполнения Promise.

  2. Глобальные индикаторы загрузки TanStack Router предоставляет доступ к состоянию маршрута и его загрузчиков через useIsFetching или usePending. Это позволяет строить глобальные индикаторы загрузки:

    import { usePending } from '@tanstack/router';
    
    function LoadingIndicator() {
      const isPending = usePending();
      return isPending ? <div>Загрузка...</div> : null;
    }

    В отличие от простого флага, usePending отслеживает все текущие асинхронные операции маршрутизатора, включая переходы и загрузки данных.

  3. Переходные состояния (transition) При навигации TanStack Router создаёт объект перехода, который отражает:

    • idle — нет активных операций,
    • pending — выполняются асинхронные операции,
    • error — произошла ошибка при загрузке.

    Использование transition позволяет синхронизировать UI с жизненным циклом маршрута:

    import { useTransition } from '@tanstack/router';
    
    function Page() {
      const transition = useTransition();
      return (
        <div>
          {transition.state === 'pending' && <span>Загрузка страницы...</span>}
        </div>
      );
    }

Управление pending для вложенных маршрутов

TanStack Router поддерживает иерархические маршруты, где один маршрут может содержать подмаршруты. Важно понимать, что pending-состояние каждого уровня маршрута можно отслеживать отдельно:

const usersRoute = createRouteConfig().path('/users').loader(fetchUsers);
const userDetailsRoute = usersRoute.createRoute().path(':id').loader(fetchUserById);
  • При переходе к /users/1 сначала сработает pending на usersRoute, затем на userDetailsRoute.
  • Для визуализации прогресса на каждом уровне используется usePending(route), где route — конкретный объект маршрута.
import { usePending } from '@tanstack/router';

function UserDetails() {
  const isPending = usePending(userDetailsRoute);
  return isPending ? <div>Загрузка данных пользователя...</div> : <UserDetailsContent />;
}

Настройка поведения pending

TanStack Router позволяет конфигурировать несколько аспектов:

  1. Задержка отображения индикатора Чтобы избежать мерцания индикатора при мгновенной загрузке, можно использовать pendingMs:

    const router = createRouter({
      routeConfig,
      pendingMs: 200, // Показывать индикатор только если загрузка > 200мс
    });
  2. Стратегии отмены запросов Асинхронные загрузчики можно отменять при навигации, чтобы не загружать устаревшие данные:

    .loader(async ({ params, signal }) => {
      const response = await fetch(`/api/users/${params.id}`, { signal });
      return response.json();
    });

    Использование AbortController позволяет автоматически завершить предыдущие запросы при изменении маршрута.


Комбинирование pending с Suspense

TanStack Router интегрируется с React Suspense для асинхронной загрузки компонентов:

const route = createRouteConfig()
  .path('/profile')
  .element(
    <React.Suspense fallback={<div>Загрузка профиля...</div>}>
      <Profile />
    </React.Suspense>
  );
  • Suspense отображает fallback, пока компонент или его данные находятся в pending-состоянии.
  • Использование loader совместно с Suspense позволяет полностью абстрагировать состояние загрузки от логики компонента.

Рекомендации по практическому использованию

  • Минимизировать мерцания: всегда использовать pendingMs или debounce для глобальных индикаторов.
  • Разделять уровни маршрутов: для больших приложений удобно отслеживать pending на уровне подмаршрутов.
  • Обрабатывать ошибки отдельно: pending и error не эквивалентны — отображение ошибки должно быть отдельным компонентом.
  • Использовать signal для отмены: предотвращает загрузку устаревших данных при быстрых навигациях.

Итоговая схема жизненного цикла pending-состояния

  1. Навигация инициирована → переход в pending.
  2. Выполняются асинхронные loader-ы → UI может показывать индикатор.
  3. Если loader завершен успешно → переход в idle и рендер контента.
  4. Если loader завершен с ошибкой → переход в error, отображение ошибки.
  5. При отмене перехода loader → Promise прерывается, pending завершается автоматически.

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