1. Введение
В реальных приложениях почти всегда есть разделы и подразделы. Например, у вас есть страница пользователя /users, а внутри — детальная страница пользователя /users/123, а ещё внутри — вкладки "Профиль", "Настройки", "История заказов" и так далее.
Вложенные маршруты позволяют:
- Организовать код и структуру приложения логично и понятно;
- Переиспользовать общий интерфейс (например, боковое меню или шапку) для целых разделов;
- Создавать вложенные layouts для разных уровней вложенности;
- Делать навигацию и загрузку данных более эффективной.
В Next.js 15 с App Router это реализуется максимально прозрачно — через структуру папок и файлов.
Принцип file-based routing
Давайте начнем с простой идеи — в Next всё строится по принципу: что в папке — то и на маршруте.
Каждая папка внутри app/ — это сегмент маршрута, а файл page.tsx внутри папки — это страница, которая рендерится по этому маршруту.
app/
users/
page.tsx // /users
[id]/
page.tsx // /users/123
settings/
page.tsx // /users/123/settings
Пояснение:
- app/users/page.tsx — страница /users
- app/users/[id]/page.tsx — страница /users/123
- app/users/[id]/settings/page.tsx — страница /users/123/settings
Важно!
Каждый уровень вложенности — это новая папка.
Динамические сегменты ([id]) работают на любом уровне.
2. Вложенные layouts: как это работает?
Layout — "скелет" для вложенных страниц
layout.tsx — это компонент, который определяет общий шаблон для всех страниц внутри данной папки.
app/
dashboard/
layout.tsx // Общий layout для dashboard
page.tsx // /dashboard
analytics/
page.tsx // /dashboard/analytics
settings/
page.tsx // /dashboard/analytics/settings
Пояснение:
- app/dashboard/layout.tsx — применяется ко всем страницам /dashboard/*
- app/dashboard/analytics/page.tsx — вложенная страница, получает layout dashboard
Пример кода layout:
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }) {
return (
<div>
<aside>Меню Dashboard</aside>
<main>{children}</main>
</div>
);
}
children — это всё, что лежит "ниже" по маршруту (страницы, вложенные layouts и т.д.).
Вложенность layouts
Layouts могут быть вложенными:
- Если у вас есть app/layout.tsx (корневой layout), он применяется ко всему приложению;
- Если есть app/dashboard/layout.tsx, он применяется ко всем страницам внутри /dashboard/*, поверх корневого layout;
- Если добавить app/dashboard/analytics/layout.tsx, он будет применяться только к /dashboard/analytics/*.
app/layout.tsx
└─ app/dashboard/layout.tsx
└─ app/dashboard/analytics/layout.tsx
└─ app/dashboard/analytics/page.tsx
3. Пример: строим вложенную структуру для учебного приложения
Допустим, мы делаем учебное приложение с разделами "Курсы", "Профиль" и "Настройки". Внутри "Курсы" есть список курсов и страница отдельного курса с вкладками "Описание" и "Материалы".
app/
layout.tsx // общий layout для всего приложения
courses/
layout.tsx // layout для раздела "Курсы"
page.tsx // /courses — список курсов
[courseId]/
layout.tsx // layout для отдельного курса
page.tsx // /courses/123 — страница курса
description/
page.tsx // /courses/123/description
materials/
page.tsx // /courses/123/materials
profile/
page.tsx // /profile
settings/
page.tsx // /settings
Что это даёт?
- На всех страницах будет шапка и футер из app/layout.tsx.
- В разделе "Курсы" (/courses/*) появится боковое меню из app/courses/layout.tsx.
- Для каждой страницы курса (/courses/123/*) можно добавить, например, меню вкладок через app/courses/[courseId]/layout.tsx.
4. Примеры: layouts и вложенные страницы
Корневой layout
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<body>
<header>Моё учебное приложение</header>
{children}
<footer>© 2024</footer>
</body>
</html>
);
}
Layout для раздела "Курсы"
// app/courses/layout.tsx
export default function CoursesLayout({ children }) {
return (
<div style={{ display: "flex" }}>
<nav>
<ul>
<li><a href="/courses">Все курсы</a></li>
<li><a href="/courses/new">Создать курс</a></li>
</ul>
</nav>
<main style={{ marginLeft: 20 }}>{children}</main>
</div>
);
}
Layout для отдельного курса (например, вкладки)
// app/courses/[courseId]/layout.tsx
export default function CourseLayout({ children, params }) {
return (
<div>
<h2>Курс #{params.courseId}</h2>
<nav>
<a href={`/courses/${params.courseId}/description`}>Описание</a> |{" "}
<a href={`/courses/${params.courseId}/materials`}>Материалы</a>
</nav>
<section>{children}</section>
</div>
);
}
Вложенные страницы
// app/courses/[courseId]/description/page.tsx
export default function DescriptionPage({ params }) {
return <div>Описание курса #{params.courseId}</div>;
}
// app/courses/[courseId]/materials/page.tsx
export default function MaterialsPage({ params }) {
return <div>Материалы курса #{params.courseId}</div>;
}
5. Полезные нюансы
Как работает рендеринг вложенных страниц
Когда пользователь заходит на /courses/123/materials, Next.js собирает страницу по следующей "матрёшке":
- app/layout.tsx — общий layout (шапка, футер)
- app/courses/layout.tsx — layout раздела "Курсы" (боковое меню)
- app/courses/[courseId]/layout.tsx — layout для конкретного курса (меню вкладок)
- app/courses/[courseId]/materials/page.tsx — содержимое страницы "Материалы"
Каждый layout "оборачивает" следующий уровень через пропс children.
Это похоже на слоёный пирог: каждый уровень добавляет свою начинку.
Схема вложенности: наглядно
/courses/123/materials
app/layout.tsx
└─ app/courses/layout.tsx
└─ app/courses/[courseId]/layout.tsx
└─ app/courses/[courseId]/materials/page.tsx
Пояснение:
- layout.tsx: общий для всего приложения
- courses/layout.tsx: для всех страниц в разделе "Курсы"
- [courseId]/layout.tsx: для всех страниц конкретного курса
- materials/page.tsx: конкретная страница ("Материалы")
Практические советы и нюансы
- Можно ли пропустить layout?
Да! Layout — опционален. Если в папке нет layout, будет использован layout из родительской папки. - Как делать вложенные динамические сегменты?
Можно! Например, /users/[userId]/posts/[postId]/page.tsx — это страница поста пользователя. - Как добавить "группы маршрутов"?
Если хотите сгруппировать папки, не влияя на URL, используйте круглые скобки:
app/(dashboard)/analytics/page.tsx — это будет /analytics, но в коде можно логически разделять части приложения. - Как работают параметры?
В каждом layout и странице вы получаете объект params с нужными значениями.
Например, в app/courses/[courseId]/materials/page.tsx
params.courseId — это ID курса. - Как делать вложенную навигацию?
Используйте <Link href="..."> из next/link для переходов без перезагрузки страницы.
6. Типичные ошибки при работе с вложенными маршрутами
Ошибка №1: неправильное расположение файлов layout/page
Иногда разработчики кладут layout.tsx на уровень, где он не нужен, или забывают создать его там, где нужен отдельный layout. В итоге либо дублируется код, либо layout применяется не к тем страницам.
Ошибка №2: забыли вложить папку для вложенного маршрута
Если вы хотите сделать маршрут /users/123/settings, но кладёте settings/page.tsx прямо в /users/, получится /users/settings, а не /users/123/settings. Для вложенности всегда нужны новые папки!
Ошибка №3: дублирование кода в layouts
Вместо того чтобы переиспользовать layouts, некоторые копируют одинаковый код в разные layouts. Лучше выносить общие части в компоненты и импортировать их.
Ошибка №4: неправильная работа с params
В layout'ах и страницах параметры берутся из объекта params. Если вы ошиблись в имени (например, написали params.id вместо params.courseId), будет ошибка или пустое значение.
Ошибка №5: попытка использовать layout как страницу
Layout не должен содержать основное содержимое страницы, а только оболочку для вложенных children. Не пишите логику страницы в layout!
Ошибка №6: забыли про children
Если в layout не рендерить {children}, вложенные страницы не появятся вообще — будет пусто. Это классика для новичков.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ