Defer для отложенной загрузки

React Router предоставляет мощный механизм для маршрутизации, но при работе с большими приложениями часто возникает необходимость отложенной загрузки данных, чтобы улучшить пользовательский опыт и ускорить рендеринг. В React Router 6.4+ появился инструмент defer, который позволяет загружать данные асинхронно и отображать страницу до завершения всех запросов.


Основы defer

Функция defer используется внутри loader-функций маршрутов для возврата объекта, содержащего как синхронные, так и асинхронные данные. Структура возвращаемого объекта может выглядеть так:

import { defer } from "react-router-dom";

export function loader() {
  return defer({
    user: fetchUser(),       // Promise
    posts: fetchPosts(),     // Promise
    timestamp: Date.now()    // Синхронное значение
  });
}
  • Синхронные данные сразу становятся доступными компоненту.
  • Асинхронные данные передаются через Promise, и их можно отображать по мере готовности.

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


Использование Await для обработки отложенных данных

Для работы с объектом, возвращённым defer, используется компонент <Await>. Он позволяет отобразить загрузку, пока данные ещё не пришли, и рендерить компонент после их получения:

import { useLoaderData, Await } from "react-router-dom";
import { Suspense } from "react";

export default function Dashboard() {
  const data = useLoaderData();

  return (
    <div>
      <h1>Панель управления</h1>
      <Suspense fallback={<p>Загрузка пользователя...</p>}>
        <Await resolve={data.user}>
          {(user) => <p>Привет, {user.name}!</p>}
        </Await>
      </Suspense>

      <Suspense fallback={<p>Загрузка постов...</p>}>
        <Await resolve={data.posts}>
          {(posts) => (
            <ul>
              {posts.map(post => (
                <li key={post.id}>{post.title}</li>
              ))}
            </ul>
          )}
        </Await>
      </Suspense>

      <p>Время запроса: {data.timestamp}</p>
    </div>
  );
}

Ключевые моменты:

  • useLoaderData() возвращает объект с данными, включая промисы.
  • Suspense указывает React, что внутри блока может быть отложенный рендер.
  • <Await> ждёт разрешения промиса и передаёт результат в рендер-функцию.

Комбинирование нескольких Await

defer позволяет одновременно работать с несколькими асинхронными запросами. Их можно обрабатывать параллельно, не блокируя общий рендер:

<Await resolve={Promise.all([data.user, data.posts])}>
  {([user, posts]) => (
    <>
      <p>{user.name}</p>
      <ul>
        {posts.map(post => <li key={post.id}>{post.title}</li>)}
      </ul>
    </>
  )}
</Await>

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


Ошибки и обработка исключений

Асинхронные данные через defer могут завершаться с ошибкой. React Router предоставляет ErrorBoundary и проп errorElement для обработки ошибок:

export const route = {
  path: "/dashboard",
  loader: loader,
  element: <Dashboard />,
  errorElement: <DashboardError />
};

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


Преимущества использования defer

  1. Быстрый первый рендер — синхронные данные сразу отображаются, а тяжелые запросы загружаются асинхронно.
  2. Удобство работы с Suspense — компоненты могут показывать индикаторы загрузки локально.
  3. Параллельная загрузка данных — несколько промисов могут выполняться одновременно без блокировки интерфейса.
  4. Чистая архитектура — loader остаётся единственным источником данных для маршрута, без необходимости разбивать компоненты на отдельные хуки для fetch-запросов.

Советы по оптимизации

  • Не использовать defer для данных, критичных для первоначального рендера, иначе пользователю придётся ждать пустой экран.
  • Разделять данные на синхронные и асинхронные для лучшего UX.
  • Комбинировать Suspense и <Await> для создания локализованных индикаторов загрузки.
  • Применять Promise.all внутри <Await> только тогда, когда нужно дождаться нескольких зависимостей одновременно.

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