React Router начиная с версии 6.4 ввёл концепцию отложенных
данных (deferred data), которая позволяет загружать данные
асинхронно и интегрировать их с рендерингом компонентов с помощью
Suspense. Это значительно упрощает работу с асинхронными
источниками данных и улучшает пользовательский опыт за счёт управления
состоянием загрузки прямо на уровне маршрутов.
defer()Функция defer() используется для создания объекта,
содержащего данные, которые могут загружаться асинхронно. Она принимает
объект с ключами, которые будут доступны в компоненте через хук
useLoaderData(). Каждый ключ может быть как
синхронным значением, так и промисом.
Пример:
import { defer } from "react-router-dom";
export function loader() {
return defer({
user: fetchUser(), // промис
settings: fetchSettings() // промис
});
}
Ключевые моменты:
defer() не блокирует рендеринг компонента.Await.AwaitAwait используется внутри компонентов, чтобы работать с
промисами, возвращёнными из defer(). Он позволяет рендерить
placeholder через Suspense до тех пор,
пока данные не загрузятся.
import { useLoaderData, Await } from "react-router-dom";
import { Suspense } from "react";
export default function UserPage() {
const data = useLoaderData();
return (
<div>
<h1>Профиль пользователя</h1>
<Suspense fallback={<p>Загрузка данных пользователя...</p>}>
<Await resolve={data.user}>
{(user) => (
<div>
<p>Имя: {user.name}</p>
<p>Email: {user.email}</p>
</div>
)}
</Await>
</Suspense>
<Suspense fallback={<p>Загрузка настроек...</p>}>
<Await resolve={data.settings}>
{(settings) => <pre>{JSON.stringify(settings, null, 2)}</pre>}
</Await>
</Suspense>
</div>
);
}
Особенности:
resolve принимает промис или отложенное значение.Await можно использовать функцию, которая
получает разрешённые данные.Suspense обеспечивает отображение запасного
UI, пока промис не завершён.Асинхронные данные удобно интегрировать с системой маршрутов. В
React Router маршруты поддерживают loader и
defer одновременно. Пример конфигурации маршрутов:
import { createBrowserRouter } from "react-router-dom";
import UserPage, { loader as userLoader } from "./UserPage";
const router = createBrowserRouter([
{
path: "/user/:id",
element: <UserPage />,
loader: userLoader
}
]);
При навигации:
loader.loader возвращает объект с defer().useLoaderData().Await с
Suspense.Await поддерживает массивы и объекты промисов
одновременно:
<Suspense fallback={<p>Загрузка...</p>}>
<Await resolve={Promise.all([fetchPosts(), fetchComments()])}>
{([posts, comments]) => (
<div>
<h2>Посты</h2>
{posts.map(post => <p key={post.id}>{post.title}</p>)}
<h2>Комментарии</h2>
{comments.map(comment => <p key={comment.id}>{comment.text}</p>)}
</div>
)}
</Await>
</Suspense>
Преимущества:
Чтобы обрабатывать ошибки асинхронных промисов, Await
поддерживает компонент ErrorBoundary или можно использовать
catch на промисе. Пример с обработкой ошибки:
<Suspense fallback={<p>Загрузка...</p>}>
<Await
resolve={data.user}
errorElement={<p>Ошибка при загрузке пользователя</p>}
>
{(user) => <p>{user.name}</p>}
</Await>
</Suspense>
Ключевые моменты:
errorElement отображается, если промис завершился с
ошибкой.Suspense:
объединять несколько небольших элементов в один блок, если они
загружаются параллельно.defer для тяжёлых данных:
крупные API-запросы, которые не нужны для начального рендера.errorElement
маршрута.Эта комбинация defer, Await и
Suspense позволяет строить высоко отзывчивые интерфейсы с
асинхронными данными, сокращает время до первого рендера и делает код
более читаемым и модульным.