Этот RFC (Request for Comment) представляет собой самое крупное обновление Next.js с момента его появления в 2016 году:
- Вложенные макеты: Создавайте сложные приложения с вложенными маршрутами.
- Оптимизировано для серверных компонентов: Улучшена навигация по поддеревьям.
- Улучшенная загрузка данных: Загрузка в макетах без водопадов запросов.
- Использование функций React 18: Потоковая передача, переходы и Suspense.
- Клиентская и серверная маршрутизация: Сервероцентричная маршрутизация с поведением, подобным SPA.
- 100% инкрементальная адаптация: Без критических изменений для постепенного внедрения.
- Продвинутые шаблоны маршрутизации: Параллельные маршруты, перехватывающие маршруты и многое другое.
Новый маршрутизатор Next.js будет построен на основе недавно выпущенных функций React 18. Мы планируем ввести стандарты и соглашения, которые позволят легко адаптировать эти новые функции и воспользоваться их преимуществами.
Работа над этим RFC продолжается, и мы объявим, когда новые функции станут доступны. Чтобы оставить отзыв, присоединяйтесь к обсуждению на Github Discussions.
Содержание
- Мотивация
- Терминология
- Как работает маршрутизация сейчас
- Директория
app
- Определение маршрутов
- Макеты
- Страницы
- Серверные компоненты React
- Загрузка данных
- Группы маршрутов (новое)
- Сервероцентричная маршрутизация (новое)
- Мгновенные состояния загрузки (новое)
- Обработка ошибок (новое)
- Шаблоны (новое)
- Продвинутые шаблоны маршрутизации (новое)
- Заключение
Мотивация
Мы собирали отзывы сообщества с GitHub, Discord, Reddit и нашего опроса разработчиков о текущих ограничениях маршрутизации в Next.js. Мы обнаружили, что:
- Опыт разработки при создании макетов можно улучшить. Должно быть легко создавать вложенные макеты, которые можно использовать на нескольких маршрутах и сохранять их состояние при навигации.
- Многие приложения Next.js — это панели управления или консоли, которые выиграют от более продвинутых решений маршрутизации.
Хотя текущая система маршрутизации хорошо работала с самого начала Next.js, мы хотим упростить разработчикам создание более производительных и функциональных веб-приложений.
Как сопровождающие фреймворка, мы также хотим создать систему маршрутизации, которая будет обратно совместима и соответствует будущему React.
Примечание: Некоторые соглашения по маршрутизации были вдохновлены маршрутизатором на основе Relay в Meta, где изначально разрабатывались некоторые функции серверных компонентов, а также клиентскими маршрутизаторами, такими как React Router и Ember.js. Соглашение о файле
layout.js
было вдохновлено работой, проделанной в SvelteKit. Мы также хотели бы поблагодарить Кэссиди за открытие более раннего RFC по макетам.
Терминология
Этот RFC вводит новые соглашения и синтаксис маршрутизации. Терминология основана на React и стандартных терминах веб-платформы. На протяжении RFC вы увидите ссылки на определения ниже.
- Дерево: Соглашение для визуализации иерархической структуры. Например, дерево компонентов с родительскими и дочерними компонентами, структура папок и т.д.
- Поддерево: Часть дерева, начинающаяся с корня (первого) и заканчивающаяся листьями (последними).
- Путь URL: Часть URL, которая идет после домена.
- Сегмент URL: Часть пути URL, разделенная слешами.
Как работает маршрутизация сейчас
Сегодня Next.js использует файловую систему для сопоставления отдельных папок и файлов в директории Pages с маршрутами, доступными через URL. Каждый файл страницы экспортирует React-компонент и имеет связанный маршрут на основе имени файла. Например:
- Динамические маршруты: Next.js поддерживает Динамические маршруты (включая варианты catch all) с соглашениями
[param].js
,[...param].js
и[[...param]].js
. - Макеты: Next.js предлагает поддержку простых макетов на основе компонентов, макетов для отдельных страниц с использованием шаблона свойства компонента и единого глобального макета с использованием пользовательского приложения.
- Загрузка данных: Next.js предоставляет методы загрузки данных (
getStaticProps
,getServerSideProps
), которые можно использовать на уровне страницы (маршрута). Эти методы используются для определения, должна ли страница быть статически сгенерирована (getStaticProps
) или отрендерена на стороне сервера (getServerSideProps
). Кроме того, вы можете использовать Инкрементальную статическую регенерацию (ISR) для создания или обновления статических страниц после сборки сайта. - Рендеринг: Next.js предоставляет три варианта рендеринга: Статическая генерация, Рендеринг на стороне сервера и Рендеринг на стороне клиента. По умолчанию страницы статически генерируются, если у них нет блокирующего требования к загрузке данных (
getServerSideProps
).
Введение в директорию app
Чтобы гарантировать, что эти новые улучшения могут быть инкрементально приняты и избежать критических изменений, мы предлагаем новую директорию под названием app
.
Директория app
будет работать вместе с директорией pages
. Вы можете постепенно перемещать части вашего приложения в новую директорию app
, чтобы воспользоваться новыми функциями. Для обратной совместимости поведение директории pages
останется прежним и будет продолжать поддерживаться.
Определение маршрутов
Вы можете использовать иерархию папок внутри app
для определения маршрутов. Маршрут — это единый путь вложенных папок, следующий иерархии от корневой папки до конечной листовой папки.
Например, вы можете добавить новый маршрут /dashboard/settings
, вложив две новые папки в директорию app
.
Примечание:
- В этой системе вы будете использовать папки для определения маршрутов, а файлы — для определения UI (с новыми соглашениями о файлах, такими как
layout.js
,page.js
, и во второй части RFCloading.js
).- Это позволяет размещать ваши собственные файлы проекта (компоненты UI, тесты, истории и т.д.) внутри директории
app
. В настоящее время это возможно только с конфигурацией pageExtensions.
Сегменты маршрута
Каждая папка в поддереве представляет сегмент маршрута. Каждый сегмент маршрута сопоставляется с соответствующим сегментом в пути URL.
Например, маршрут /dashboard/settings
состоит из 3 сегментов:
- Корневой сегмент
/
- Сегмент
dashboard
- Сегмент
settings
Примечание: Название сегмент маршрута было выбрано для соответствия существующей терминологии вокруг путей URL.
Макеты
Новое соглашение о файлах: layout.js
До сих пор мы использовали папки для определения маршрутов нашего приложения. Но пустые папки сами по себе ничего не делают. Давайте обсудим, как вы можете определить UI, который будет отображаться для этих маршрутов, используя новые соглашения о файлах.
Макет — это UI, общий для сегментов маршрута в поддереве. Макеты не влияют на пути URL и не перерендериваются (состояние React сохраняется), когда пользователь переходит между родственными сегментами.
Макет можно определить, экспортировав React-компонент по умолчанию из файла layout.js
. Компонент должен принимать проп children
, который будет заполнен сегментами, которые макет оборачивает.
Существует 2 типа макетов:
- Корневой макет: Применяется ко всем маршрутам
- Обычный макет: Применяется к определенным маршрутам
Вы можете вложить два или более макетов вместе, чтобы сформировать вложенные макеты.
Корневой макет
Вы можете создать корневой макет, который будет применяться ко всем маршрутам вашего приложения, добавив файл layout.js
внутри папки app
.
Примечание:
- Корневой макет заменяет необходимость в пользовательском приложении (
_app.js
) и пользовательском документе (_document.js
), поскольку он применяется ко всем маршрутам.- Вы сможете использовать корневой макет для настройки начальной оболочки документа (например, тегов
<html>
и<body>
).- Вы сможете загружать данные внутри корневого макета (и других макетов).
Обычные макеты
Вы также можете создать макет, который применяется только к части вашего приложения, добавив файл layout.js
внутри конкретной папки.
Например, вы можете создать файл layout.js
внутри папки dashboard
, который будет применяться только к сегментам маршрута внутри dashboard
.
Вложенные макеты
Макеты по умолчанию вложены.
Например, если мы объединим два макета выше. Корневой макет (app/layout.js
) будет применен к макету dashboard
, который также будет применен ко всем сегментам маршрута внутри dashboard/*
.
Страницы
Новое соглашение о файлах: page.js
Страница — это UI, уникальный для сегмента маршрута. Вы можете создать страницу, добавив файл page.js
внутри папки.
Например, чтобы создать страницы для маршрутов /dashboard/*
, вы можете добавить файл page.js
внутри каждой папки. Когда пользователь посещает /dashboard/settings
, Next.js отрендерит файл page.js
для папки settings
, обернутый в любые макеты, существующие выше по поддереву.
Вы можете создать файл page.js
непосредственно внутри папки dashboard, чтобы соответствовать маршруту /dashboard
. Макет dashboard также будет применен к этой странице:
Этот маршрут состоит из 2 сегментов:
- Корневой сегмент
/
- Сегмент
dashboard
Примечание:
- Чтобы маршрут был валидным, он должен иметь страницу в своем листовом сегменте. Если её нет, маршрут вызовет ошибку.
Поведение макета и страницы
- Расширения файлов
js|jsx|ts|tsx
могут использоваться для страниц и макетов. - Компоненты страниц — это экспорт по умолчанию из
page.js
. - Компоненты макетов — это экспорт по умолчанию из
layout.js
. - Компоненты макетов должны принимать проп
children
.
Когда компонент макета рендерится, проп children
будет заполнен дочерним макетом (если он существует дальше по поддереву) или страницей.
Это может быть проще визуализировать как дерево макетов, где родительский макет будет выбирать ближайший дочерний макет, пока не достигнет страницы.
Пример:
// Корневой макет
// - Применяется ко всем маршрутам
export default function RootLayout({ children }) {
return (
<html>
<body>
<Header />
{children}
<Footer />
</body>
</html>
);
}
// Обычный макет
// - Применяется к сегментам маршрута в app/dashboard/*
export default function DashboardLayout({ children }) {
return (
<>
<DashboardSidebar />
{children}
</>
);
}
// Компонент страницы
// - UI для сегмента `app/dashboard/analytics`
// - Соответствует пути URL `acme.com/dashboard/analytics`
export default function AnalyticsPage() {
return <main>...</main>;
}
Вышеуказанная комбинация макетов и страниц отрендерит следующую иерархию компонентов:
<RootLayout>
<Header />
<DashboardLayout>
<DashboardSidebar />
<AnalyticsPage>
<main>...</main>
</AnalyticsPage>
</DashboardLayout>
<Footer />
</RootLayout>
Серверные компоненты React
Примечание: React представил новые типы компонентов: серверные, клиентские (традиционные React-компоненты) и общие. Чтобы узнать больше об этих новых типах, мы рекомендуем прочитать RFC по серверным компонентам React.
С этим RFC вы можете начать использовать функции React и постепенно внедрять серверные компоненты React в ваше приложение Next.js.
Внутренняя часть новой системы маршрутизации также будет использовать недавно выпущенные функции React, такие как потоковая передача, Suspense и переходы. Это строительные блоки для серверных компонентов React.
Серверные компоненты по умолчанию
Одно из самых больших изменений между директориями pages
и app
заключается в том, что по умолчанию файлы внутри app
будут рендериться на сервере как серверные компоненты React.
Это позволит вам автоматически использовать серверные компоненты React при переходе с pages
на app
.
Примечание: Серверные компоненты можно использовать в папке
app
или ваших собственных папках, но нельзя использовать в директорииpages
для обратной совместимости.
Конвенция клиентских и серверных компонентов
Папка app
будет поддерживать серверные, клиентские и общие компоненты, и вы сможете перемежать эти компоненты в дереве.
В настоящее время ведётся обсуждение того, какой именно будет конвенция для определения клиентских и серверных компонентов. Мы будем следовать результатам этого обсуждения.
- На данный момент серверные компоненты можно определить, добавив
.server.js
к имени файла. Например:layout.server.js
- Клиентские компоненты можно определить, добавив
.client.js
к имени файла. Например:page.client.js
- Файлы с расширением
.js
считаются общими компонентами. Поскольку они могут рендериться как на сервере, так и на клиенте, они должны учитывать ограничения каждого контекста.
Примечание:
- Клиентские и серверные компоненты имеют ограничения, которые необходимо соблюдать. При выборе между клиентским и серверным компонентом мы рекомендуем использовать серверные компоненты (по умолчанию), пока вам не понадобится клиентский компонент.
Хуки
Мы добавим хуки для клиентских и серверных компонентов, которые позволят вам получать доступ к объекту заголовков, кукам, путям, параметрам поиска и т.д. В будущем мы предоставим документацию с более подробной информацией.
Среды рендеринга
Вы получите детальный контроль над тем, какие компоненты будут включены в клиентский JavaScript-бандл, используя конвенцию клиентских и серверных компонентов.
По умолчанию маршруты в app
будут использовать статическую генерацию и переключаться на динамический рендеринг, когда сегмент маршрута использует серверные хуки, требующие контекста запроса.
Перемежение клиентских и серверных компонентов в маршруте
В React есть ограничение на импорт серверных компонентов внутри клиентских, поскольку серверные компоненты могут содержать код, предназначенный только для сервера (например, утилиты для работы с базой данных или файловой системой).
Например, импорт серверного компонента не сработает:
import ServerComponent from './ServerComponent.js';
export default function ClientComponent() {
return (
<>
<ServerComponent />
</>
);
}
Однако серверный компонент можно передать как дочерний элемент клиентского компонента. Это можно сделать, обернув их в другой серверный компонент. Например:
export default function ClientComponent({ children }) {
return (
<>
<h1>Клиентский компонент</h1>
{children}
</>
);
}
// ServerComponent.js
export default function ServerComponent() {
return (
<>
<h1>Серверный компонент</h1>
</>
);
}
// page.js
// Можно импортировать клиентские и серверные компоненты внутри серверных,
// потому что этот компонент рендерится на сервере
import ClientComponent from "./ClientComponent.js";
import ServerComponent from "./ServerComponent.js";
export default function ServerComponentPage() {
return (
<>
<ClientComponent>
<ServerComponent />
</ClientComponent>
</>
);
}
С таким подходом React будет знать, что ему нужно отрендерить ServerComponent
на сервере перед отправкой результата (который не содержит серверного кода) клиенту. С точки зрения клиентского компонента, его дочерний элемент уже будет отрендерен.
В макетах этот паттерн применяется с пропсом children
, поэтому вам не нужно создавать дополнительный компонент-обёртку.
Например, компонент ClientLayout
примет компонент ServerPage
как своего дочернего элемента:
// Макет Dashboard — это клиентский компонент
export default function ClientLayout({ children }) {
// Можно использовать useState / useEffect здесь
return (
<>
<h1>Макет</h1>
{children}
</>
);
}
// Страница — это серверный компонент, который будет передан в макет Dashboard
// app/dashboard/settings/page.js
export default function ServerPage() {
return (
<>
<h1>Страница</h1>
</>
);
}
Примечание: Такой стиль композиции — это важный паттерн для рендеринга серверных компонентов внутри клиентских. Он устанавливает приоритет одного паттерна для изучения и является одной из причин, почему мы решили использовать проп
children
.
Загрузка данных
Будет возможна загрузка данных внутри нескольких сегментов маршрута. Это отличается от директории pages
, где загрузка данных была ограничена уровнем страницы.
Загрузка данных в макетах
Вы можете загружать данные в файле layout.js
, используя методы Next.js для загрузки данных getStaticProps
или getServerSideProps
.
Например, макет блога может использовать getStaticProps
для загрузки категорий из CMS, которые можно использовать для заполнения боковой панели:
export async function getStaticProps() {
const categories = await getCategoriesFromCMS();
return {
props: { categories },
};
}
export default function BlogLayout({ categories, children }) {
return (
<>
<BlogSidebar categories={categories} />
{children}
</>
);
}
Несколько методов загрузки данных в маршруте
Вы также можете загружать данные в нескольких сегментах маршрута. Например, макет, который загружает данные, может оборачивать страницу, которая загружает свои собственные данные.
Используя пример с блогом выше, страница отдельного поста может использовать getStaticProps
и getStaticPaths
для загрузки данных поста из CMS:
export async function getStaticPaths() {
const posts = await getPostSlugsFromCMS();
return {
paths: posts.map((post) => ({
params: { slug: post.slug },
})),
};
}
export async function getStaticProps({ params }) {
const post = await getPostFromCMS(params.slug);
return {
props: { post },
};
}
export default function BlogPostPage({ post }) {
return <Post post={post} />;
}
Поскольку и app/blog/layout.js
, и app/blog/[slug]/page.js
используют getStaticProps
, Next.js будет статически генерировать весь маршрут /blog/[slug]
как серверные компоненты React во время сборки — это уменьшит количество клиентского JavaScript и ускорит гидратацию.
Статически сгенерированные маршруты улучшают это ещё больше, так как клиентская навигация повторно использует кеш (данные серверных компонентов) и не пересчитывает работу, что приводит к уменьшению времени CPU, потому что вы рендерите снимок серверных компонентов.
Поведение и приоритет
Методы загрузки данных Next.js (getServerSideProps
и getStaticProps
) можно использовать только в серверных компонентах в папке app
. Разные методы загрузки данных в сегментах одного маршрута влияют друг на друга.
Использование getServerSideProps
в одном сегменте повлияет на getStaticProps
в других сегментах. Поскольку запрос уже должен отправиться на сервер для сегмента с getServerSideProps
, сервер также отрендерит все сегменты с getStaticProps
. Он повторно использует пропсы, загруженные во время сборки, поэтому данные останутся статическими, но рендеринг будет происходить по запросу с пропсами, сгенерированными во время next build
.
Использование getStaticProps
с revalidate (ISR) в одном сегменте повлияет на getStaticProps
с revalidate
в других сегментах. Если в одном маршруте есть два периода ревалидации, будет использоваться более короткий.
Примечание: В будущем это может быть оптимизировано для полной детализации загрузки данных в маршруте.
Загрузка данных с серверными компонентами React
Комбинация серверной маршрутизации, серверных компонентов React, Suspense и потоковой передачи имеет несколько последствий для загрузки данных и рендеринга в Next.js:
Параллельная загрузка данных
Next.js будет активно инициировать загрузку данных параллельно, чтобы минимизировать водопады. Например, если бы загрузка данных была последовательной, каждый вложенный сегмент маршрута не мог бы начать загрузку данных, пока предыдущий сегмент не завершился бы. Однако при параллельной загрузке каждый сегмент может начать загрузку данных одновременно.
Поскольку рендеринг может зависеть от контекста, рендеринг каждого сегмента начнётся после загрузки его данных и завершения рендеринга родителя.
В будущем, с Suspense, рендеринг также сможет начаться немедленно — даже если данные ещё не полностью загружены. Если данные будут прочитаны до их доступности, сработает Suspense. React начнёт рендерить серверные компоненты оптимистично, до завершения запросов, и вставит результат по мере их разрешения.
Частичная загрузка и рендеринг
При навигации между соседними сегментами маршрута Next.js будет загружать и рендерить только начиная с этого сегмента. Ему не нужно будет перезагружать или перерендеривать что-либо выше. Это означает, что на странице, которая использует общий макет, макет сохранится при навигации между соседними страницами, и Next.js будет загружать и рендерить только начиная с этого сегмента.
Это особенно полезно для серверных компонентов React, так как иначе каждая навигация вызывала бы полный перерендер страницы на сервере вместо рендеринга только изменённой части страницы. Это уменьшает объём передаваемых данных и время выполнения, что улучшает производительность.
Например, если пользователь переходит между страницами /analytics
и /settings
, React перерендерит сегменты страницы, но сохранит макеты:
Примечание: Будет возможным принудительно перезагрузить данные выше по дереву. Мы всё ещё обсуждаем детали того, как это будет выглядеть, и обновим RFC.
Группы маршрутов
Иерархия папки app
напрямую соответствует URL-путям. Но можно выйти за рамки этого шаблона, создав группу маршрутов. Группы маршрутов можно использовать для:
- Организации маршрутов без влияния на структуру URL.
- Исключения сегмента маршрута из макета.
- Создания нескольких корневых макетов путём разделения приложения.
Конвенция
Группу маршрутов можно создать, заключив имя папки в скобки: (folderName)
Примечание: Именование групп маршрутов служит только для организационных целей, так как не влияет на URL-путь.
Пример: Исключение маршрута из макета
Чтобы исключить маршрут из макета, создайте новую группу маршрутов (например, (shop)
) и переместите маршруты, которые используют общий макет, в эту группу (например, account
и cart
). Маршруты вне группы не будут использовать этот макет (например, checkout
).
До:
После:
Пример: Организация маршрутов без влияния на URL
Аналогично, для организации маршрутов создайте группу, чтобы держать связанные маршруты вместе. Папки в скобках будут исключены из URL (например, (marketing)
или (shop)
).
Пример: Создание нескольких корневых макетов
Чтобы создать несколько корневых макетов, создайте две или более группы маршрутов на верхнем уровне папки app
. Это полезно для разделения приложения на секции с совершенно разным UI или опытом. Теги <html>
, <body>
и <head>
каждого корневого макета можно настраивать отдельно.
Сервероцентричная маршрутизация
В настоящее время Next.js использует клиентскую маршрутизацию. После первоначальной загрузки и при последующей навигации отправляется запрос на сервер для ресурсов новой страницы. Это включает JavaScript для каждого компонента (включая компоненты, отображаемые только при определённых условиях) и их пропсы (JSON-данные из getServerSideProps
или getStaticProps
). Как только JavaScript и данные загружены с сервера, React рендерит компоненты на клиенте.
В этой новой модели Next.js будет использовать сервероцентричную маршрутизацию, сохраняя клиентские переходы. Это согласуется с серверными компонентами, которые вычисляются на сервере.
При навигации данные загружаются, и React рендерит компоненты на сервере. Результат с сервера — это специальные инструкции (не HTML или JSON) для React на клиенте, чтобы обновить DOM. Эти инструкции содержат результат рендеринга серверных компонентов, что означает, что не нужно загружать JavaScript для этого компонента в браузер для рендеринга результата.
Это отличается от текущего поведения по умолчанию с клиентскими компонентами, где JavaScript компонента отправляется в браузер для рендеринга на клиенте.
Некоторые преимущества сервероцентричной маршрутизации с серверными компонентами React:
- Маршрутизация использует тот же запрос, что и для серверных компонентов (дополнительные запросы на сервер не делаются).
- На сервере выполняется меньше работы, потому что навигация между маршрутами загружает и рендерит только изменяющиеся сегменты.
- В браузер не загружается дополнительный JavaScript при клиентской навигации, если не используются новые клиентские компоненты.
- Маршрутизатор использует новый протокол потоковой передачи, чтобы рендеринг мог начаться до загрузки всех данных.
Когда пользователи перемещаются по приложению, маршрутизатор будет хранить результат полезной нагрузки серверных компонентов React в клиентском кеше в памяти. Кеш разделён по сегментам маршрута, что позволяет инвалидировать любой уровень и обеспечивает согласованность при параллельных рендерах. Это означает, что в некоторых случаях можно повторно использовать кеш ранее загруженного сегмента.
Примечание
- Статическая генерация и серверное кеширование могут использоваться для оптимизации загрузки данных.
- Информация выше описывает поведение последующих навигаций. Первоначальная загрузка — это другой процесс, включающий серверный рендеринг для генерации HTML.
- Хотя клиентская маршрутизация хорошо работала для Next.js, она плохо масштабируется, когда количество потенциальных маршрутов велико, потому что клиент должен загружать карту маршрутов.
- В целом, используя серверные компоненты React, клиентская навигация становится быстрее, потому что мы загружаем и рендерим меньше компонентов в браузере.
Мгновенные состояния загрузки
При серверной маршрутизации навигация происходит после загрузки данных и рендеринга, поэтому важно показывать состояние загрузки, пока данные загружаются, иначе приложение будет казаться неотзывчивым.
Новый маршрутизатор будет использовать Suspense для мгновенных состояний загрузки и стандартных скелетонов. Это означает, что состояние загрузки можно показать немедленно, пока загружается контент для нового сегмента. Новый контент затем заменяется, когда рендеринг на сервере завершён.
Во время рендеринга:
- Навигация на новый маршрут будет мгновенной.
- Общие макеты останутся интерактивными, пока загружаются новые сегменты маршрута.
- Навигация будет прерываемой — пользователь сможет переключаться между маршрутами, пока загружается контент одного из них.
Стандартные скелетоны загрузки
Границы Suspense будут автоматически обрабатываться с помощью новой файловой конвенции loading.js
.
Пример:
Вы сможете создать стандартный скелетон загрузки, добавив файл loading.js
внутри папки.
Файл loading.js
должен экспортировать React-компонент:
export default function Loading() {
return <YourSkeleton />
}
// layout.js
export default function Layout({children}) {
return (
<>
<Sidebar />
{children}
</>
)
}
// Output
<>
<Sidebar />
<Suspense fallback={<Loading />}>{children}</Suspense>
</>
Это приведёт к тому, что все сегменты в папке будут обёрнуты в границу Suspense. Стандартный скелетон будет использоваться при первой загрузке макета и при навигации между страницами-сестрами.
Обработка ошибок
Границы ошибок (Error boundaries) — это React-компоненты, которые перехватывают JavaScript-ошибки в любом месте дерева дочерних компонентов.
Конвенция
Вы сможете создать границу ошибок, которая будет перехватывать ошибки внутри поддерева, добавив файл error.js
и экспортировав по умолчанию React-компонент.
Компонент будет показан как фолбэк, если в этом поддереве возникнет ошибка. Этот компонент можно использовать для логирования ошибок, отображения полезной информации об ошибке и функциональности для попытки восстановления после ошибки.
Благодаря вложенной природе сегментов и макетов, создание границ ошибок позволяет изолировать ошибки в определённых частях UI. При возникновении ошибки макеты выше границы останутся интерактивными, и их состояние сохранится.
export default function Error({ error, reset }) {
return (
<>
Произошла ошибка: {error.message}
<button onClick={() => reset()}>Попробовать снова</button>
</>
);
}
// layout.js
export default function Layout({children}) {
return (
<>
<Sidebar />
{children}
</>
)
}
// Output
<>
<Sidebar />
<ErrorBoundary fallback={<Error />}>{children}</ErrorBoundary>
</>
Примечание:
- Ошибки внутри файла
layout.js
в том же сегменте, что иerror.js
, не будут перехвачены, так как автоматическая граница ошибок оборачивает дочерние элементы макета, а не сам макет.
Шаблоны (Templates)
Шаблоны похожи на макеты тем, что они оборачивают каждый дочерний макет или страницу.
В отличие от макетов, которые сохраняются между маршрутами и поддерживают состояние, шаблоны создают новый экземпляр для каждого из своих дочерних элементов. Это означает, что при навигации пользователя между сегментами маршрута, которые используют один шаблон, монтируется новый экземпляр компонента.
Рекомендация: Используйте макеты, если у вас нет конкретной причины использовать шаблон.
Конвенция
Шаблон можно определить, экспортировав React-компонент по умолчанию из файла template.js
. Компонент должен принимать проп children
, который будет заполнен вложенными сегментами.
Пример
export default function Template({ children }) {
return <Container>{children}</Container>;
}
Результат рендеринга сегмента маршрута с макетом и шаблоном будет выглядеть так:
<Layout>
{/* Обратите внимание, что шаблону присваивается уникальный ключ. */}
<Template key={routeParam}>{children}</Template>
</Layout>
Поведение
Бывают случаи, когда вам нужно монтировать и размонтировать общий UI, и шаблоны будут более подходящим вариантом. Например:
- Анимации входа/выхода с использованием CSS или библиотек анимации
- Функции, зависящие от
useEffect
(например, логирование просмотров страниц) иuseState
(например, форма обратной связи для каждой страницы) - Чтобы изменить поведение фреймворка по умолчанию. Например, границы Suspense внутри макетов показывают фолбэк только при первой загрузке макета, а не при переключении страниц. Для шаблонов фолбэк показывается при каждой навигации.
Например, рассмотрим дизайн вложенного макета с контейнером с рамкой, который должен оборачивать каждую подстраницу.
Вы можете поместить контейнер внутри родительского макета (shop/layout.js
):
export default function Layout({ children }) {
return <div className="container">{children}</div>;
}
// shop/page.js
export default function Page() {
return <div>...</div>;
}
// shop/categories/layout.js
export default function CategoryLayout({ children }) {
return <div>{children}</div>;
}
Но любые анимации входа/выхода не будут проигрываться при переключении страниц, потому что общий родительский макет не перерендеривается.
Вы можете поместить контейнер в каждый вложенный макет или страницу:
export default function Layout({ children }) {
return <div>{children}</div>;
}
// shop/page.js
export default function Page() {
return <div className="container">...</div>;
}
// shop/categories/layout.js
export default function CategoryLayout({ children }) {
return <div className="container">{children}</div>;
}
Но тогда вам придётся вручную добавлять его в каждый вложенный макет или страницу, что может быть утомительным и подверженным ошибкам в более сложных приложениях.
С этой конвенцией вы можете использовать общие шаблоны для маршрутов, которые создают новый экземпляр при навигации. Это означает, что DOM-элементы будут пересозданы, состояние не сохранится, а эффекты будут повторно синхронизированы.
Продвинутые паттерны маршрутизации
Мы планируем ввести конвенции для обработки крайних случаев и позволить вам реализовать более продвинутые паттерны маршрутизации. Ниже приведены некоторые примеры, над которыми мы активно работаем:
Перехватывающие маршруты (Intercepting Routes)
Иногда может быть полезно перехватывать сегменты маршрута из других маршрутов. При навигации URL будет обновляться как обычно, но перехваченный сегмент будет показан внутри макета текущего маршрута.
Пример
До: Клик по изображению ведёт на новый маршрут с собственным макетом.
После: Перехватив маршрут, клик по изображению теперь загружает сегмент внутри макета текущего маршрута. Например, как модальное окно.
Чтобы перехватить маршрут /photo/[id]
из сегмента /[username]
, создайте дублирующую папку /photo/[id]
внутри папки /[username]
и добавьте префикс (..)
.
Конвенция
(..)
— соответствует сегменту маршрута на один уровень выше (сестринскому элементу родительской директории). Аналогично../
в относительных путях.(..)(..)
— соответствует сегменту маршрута на два уровня выше. Аналогично../../
в относительных путях.(...)
— соответствует сегменту маршрута в корневой директории.
Примечание: Обновление страницы или её открытие по ссылке загрузит маршрут с его стандартным макетом.
Динамические параллельные маршруты (Dynamic Parallel Routes)
Иногда может быть полезно показывать два или более конечных сегмента (page.js
) в одном представлении, между которыми можно переключаться независимо.
Например, две или более групп вкладок в одной панели управления. Переключение одной группы вкладок не должно влиять на другую. Комбинации вкладок также должны корректно восстанавливаться при навигации назад и вперёд.
Конвенция
По умолчанию макеты принимают проп children
, который содержит вложенный макет или страницу. Вы можете переименовать этот проп, создав именованный "слот" (папку с префиксом @
) и вложив в неё сегменты.
После этого изменения макет будет получать проп customProp
вместо children
.
export default function Layout({ customProp }) {
return <>{customProp}</>;
}
Вы можете создать параллельные маршруты, добавив более одного именованного слота на одном уровне. В примере ниже и @views
, и @audience
передаются как пропсы в макет analytics.
Вы можете использовать именованные слоты для одновременного отображения конечных сегментов.
export default function Layout({ views, audience }) {
return (
<>
<div>
<ViewsNav />
{views}
</div>
<div>
<AudienceNav />
{audience}
</div>
</>
);
}
Когда пользователь впервые переходит на /analytics
, отображаются сегменты page.js
в каждой папке (@views
и @audience
).
При переходе на /analytics/subscribers
обновляется только @audience
. Аналогично, только @views
обновляется при переходе на /analytics/impressions
.
Навигация назад и вперёд восстановит правильную комбинацию параллельных маршрутов.
Комбинирование перехватывающих и параллельных маршрутов
Вы можете комбинировать перехватывающие и параллельные маршруты для достижения специфического поведения маршрутизации в вашем приложении.
Пример
Например, при создании модального окна часто приходится сталкиваться с такими проблемами, как:
- Модальные окна недоступны через URL.
- Модальные окна закрываются при обновлении страницы.
- Навигация назад ведёт на предыдущий маршрут, а не на маршрут за модальным окном.
- Навигация вперёд не открывает модальное окно снова.
Возможно, вы захотите, чтобы модальное окно обновляло URL при открытии, а навигация назад/вперёд открывала и закрывала его. Кроме того, при совместном использовании URL вы можете захотеть, чтобы страница загружалась с открытым модальным окном и контекстом за ним или без модального окна.
Хороший пример этого — фотографии в социальных сетях. Обычно фотографии доступны в модальном окне из ленты пользователя или профиля. Но при совместном использовании фотографии отображаются непосредственно на своей собственной странице.
Используя конвенции, мы можем сделать поведение модального окна соответствующим поведению маршрутизации по умолчанию.
Рассмотрим эту структуру папок:
С этим паттерном:
- Содержимое
/photo/[id]
доступно через URL в собственном контексте. Оно также доступно в модальном окне из маршрута/[username]
. - Навигация назад и вперёд с использованием клиентской навигации должна закрывать и открывать модальное окно.
- Обновление страницы (серверная навигация) должно перенаправлять пользователя на оригинальный маршрут
/photo/id
вместо показа модального окна.
В /@modal/(..)photo/[id]/page.js
вы можете вернуть содержимое страницы, обёрнутое в компонент модального окна.
export default function PhotoPage() {
const router = useRouter();
return (
<Modal
// модальное окно всегда должно показываться при загрузке страницы
isOpen={true}
// закрытие модального окна должно вернуть пользователя на предыдущую страницу
onClose={() => router.back()}
>
{/* Содержимое страницы */}
</Modal>
);
}
Примечание: Это решение не единственный способ создания модального окна в Next.js, но оно демонстрирует, как можно комбинировать конвенции для достижения более сложного поведения маршрутизации.
Условные маршруты (Conditional Routes)
Иногда вам может потребоваться динамическая информация, такая как данные или контекст, чтобы определить, какой маршрут показывать. Вы можете использовать параллельные маршруты для условной загрузки одного маршрута или другого.
Пример
export async function getServerSideProps({ params }) {
const { accountType } = await fetchAccount(params.slug);
return { props: { isUser: accountType === 'user' } };
}
export default function UserOrTeamLayout({ isUser, user, team }) {
return <>{isUser ? user : team}</>;
}
В примере выше вы можете вернуть либо маршрут user
, либо team
в зависимости от slug. Это позволяет вам условно загружать данные и сопоставлять подмаршруты с одним из вариантов.
Заключение
Мы вдохновлены будущим макетов, маршрутизации и React 18 в Next.js. Работа над реализацией уже началась, и мы объявим о функциях, как только они станут доступны.
Оставляйте комментарии и присоединяйтесь к обсуждению на GitHub Discussions.