Пошук уроків, статей та іншого контенту
Організуйте layouts для вкладених сегментів маршруту та збережіть стан інтерфейсу під час навігації.
У Next.js з App Router layout — це компонент, який обгортає сторінки певного сегмента маршруту.
Layout можна створити файлом layout.tsx у папці app:
app/
├── layout.tsx
├── page.tsx
└── dashboard/
├── layout.tsx
├── page.tsx
└── settings/
└── page.tsxУ цьому прикладі:
app/layout.tsx — кореневий layout для всього застосунку;
app/dashboard/layout.tsx — layout для всіх сторінок усередині /dashboard;
app/dashboard/page.tsx — сторінка /dashboard;
app/dashboard/settings/page.tsx — сторінка /dashboard/settings.
Під час відображення /dashboard/settings компоненти вкладаються так:
RootLayout
└── DashboardLayout
└── SettingsPageКореневий layout є обов’язковим для App Router. Він має містити HTML-структуру документа: <html> і <body>.
app/layout.tsximport type { ReactNode } from "react";
export default function RootLayout({
children,
}: {
children: ReactNode;
}) {
return (
<html lang="uk">
<body>{children}</body>
</html>
);
}Параметр children — це поточна сторінка або вкладений layout.
Наприклад, для маршруту /dashboard значенням children кореневого layout буде DashboardLayout, а для маршруту / — компонент із app/page.tsx.
Щоб створити layout для сегмента /dashboard, додайте файл app/dashboard/layout.tsx.
app/dashboard/layout.tsximport type { ReactNode } from "react";
import Sidebar from "./Sidebar";
export default function DashboardLayout({
children,
}: {
children: ReactNode;
}) {
return (
<div>
<header>
<h1>Панель керування</h1>
</header>
<div>
<Sidebar />
<main>{children}</main>
</div>
</div>
);
}Цей layout буде використовуватися для:
/dashboard;
/dashboard/settings;
/dashboard/reports;
будь-яких інших сторінок усередині папки app/dashboard.
Водночас DashboardLayout не застосовується до сторінок поза цією папкою, наприклад до /about.
Розглянемо невеликий dashboard із бічною панеллю навігації.
app/
├── layout.tsx
├── page.tsx
└── dashboard/
├── layout.tsx
├── Sidebar.tsx
├── page.tsx
├── settings/
│ └── page.tsx
└── reports/
└── page.tsxapp/layout.tsximport type { ReactNode } from "react";
export default function RootLayout({
children,
}: {
children: ReactNode;
}) {
return (
<html lang="uk">
<body>{children}</body>
</html>
);
}app/page.tsximport Link from "next/link";
export default function HomePage() {
return (
<main>
<h1>Головна сторінка</h1>
<Link href="/dashboard">Відкрити dashboard</Link>
</main>
);
}app/dashboard/layout.tsximport type { ReactNode } from "react";
import Sidebar from "./Sidebar";
export default function DashboardLayout({
children,
}: {
children: ReactNode;
}) {
return (
<section>
<header>
<h1>Панель керування</h1>
</header>
<div>
<Sidebar />
<main>{children}</main>
</div>
</section>
);
}app/dashboard/Sidebar.tsx"use client";
import { useState } from "react";
import Link from "next/link";
export default function Sidebar() {
const [isOpen, setIsOpen] = useState(true);
return (
<aside>
<button type="button" onClick={() => setIsOpen((value) => !value)}>
{isOpen ? "Сховати меню" : "Показати меню"}
</button>
{isOpen && (
<nav>
<ul>
<li>
<Link href="/dashboard">Огляд</Link>
</li>
<li>
<Link href="/dashboard/settings">Налаштування</Link>
</li>
<li>
<Link href="/dashboard/reports">Звіти</Link>
</li>
</ul>
</nav>
)}
</aside>
);
}Sidebar є клієнтським компонентом, тому що використовує useState.
app/dashboard/page.tsxexport default function DashboardPage() {
return (
<div>
<h2>Огляд</h2>
<p>Загальна інформація про застосунок.</p>
</div>
);
}app/dashboard/settings/page.tsxexport default function SettingsPage() {
return (
<div>
<h2>Налаштування</h2>
<p>Тут можна змінити налаштування профілю.</p>
</div>
);
}app/dashboard/reports/page.tsxexport default function ReportsPage() {
return (
<div>
<h2>Звіти</h2>
<p>Тут відображаються звіти.</p>
</div>
);
}Тепер при переході між /dashboard, /dashboard/settings і /dashboard/reports:
DashboardLayout залишається змонтованим;
заголовок і Sidebar не створюються заново;
стан isOpen у Sidebar зберігається;
змінюється лише вміст children.
Layouts зберігають свій стан під час навігації між сторінками в межах того самого сегмента маршруту.
Наприклад, користувач:
відкриває /dashboard;
натискає «Сховати меню»;
переходить на /dashboard/settings.
Оскільки обидві сторінки використовують app/dashboard/layout.tsx, компонент Sidebar не демонтується. Меню залишається прихованим.
Під час переходу на маршрут поза /dashboard, наприклад /, DashboardLayout більше не потрібен. Він буде демонтований, а його локальний стан втратить значення.
Також стан не зберігається після повного перезавантаження сторінки браузером, якщо його окремо не зберігати, наприклад у cookie або іншому сховищі.
childrenLayout не визначає конкретну сторінку самостійно. Поточний вміст передається через children.
export default function SectionLayout({
children,
}: {
children: ReactNode;
}) {
return (
<div>
<nav>Навігація розділу</nav>
{children}
</div>
);
}Для маршруту /dashboard/settings:
children у RootLayout містить DashboardLayout;
children у DashboardLayout містить SettingsPage.
Тому вкладені layouts утворюють дерево компонентів відповідно до структури папок.
LinkДля переходів між сторінками використовуйте Link з next/link:
import Link from "next/link";
export default function Navigation() {
return (
<nav>
<Link href="/dashboard">Огляд</Link>
<Link href="/dashboard/settings">Налаштування</Link>
</nav>
);
}Під час переходу через Link Next.js виконує клієнтську навігацію. Це дає змогу зберігати спільні layouts і їхній стан.
Повне перезавантаження сторінки має іншу поведінку: застосунок завантажується знову, тому локальний стан компонентів починається з початкового значення.
Layouts за замовчуванням є Server Components. Це означає, що в layout не можна безпосередньо використовувати React-хуки, наприклад useState.
Якщо частині інтерфейсу потрібен стан або обробники подій, винесіть її в окремий клієнтський компонент:
"use client";
import { useState } from "react";
export default function Toggle() {
const [enabled, setEnabled] = useState(false);
return (
<button type="button" onClick={() => setEnabled((value) => !value)}>
{enabled ? "Увімкнено" : "Вимкнено"}
</button>
);
}Потім підключіть його до layout:
import Toggle from "./Toggle";
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<Toggle />
{children}
</div>
);
}Не потрібно робити весь layout клієнтським лише через одну інтерактивну кнопку. Краще залишити layout Server Component, а інтерактивну частину винести в окремий компонент.
Файл app/dashboard/layout.tsx працює для маршрутів усередині /dashboard.
Файл app/layout.tsx працює для всього застосунку. Якщо layout має бути спільним лише для певного розділу, його потрібно розмістити в папці цього розділу.
childrenЯкщо не вставити {children}, сторінки, вкладені в layout, не відобразяться.
export default function Layout({
children,
}: {
children: React.ReactNode;
}) {
return <div>{children}</div>;
}useState у Server ComponentТакий код не працюватиме без директиви "use client":
import { useState } from "react";
export default function Sidebar() {
const [open, setOpen] = useState(true);
return null;
}Для компонента зі станом додайте "use client" на початку файлу або винесіть стан в окремий клієнтський компонент.
Layout зберігає стан під час клієнтської навігації між сумісними маршрутами. Але після оновлення сторінки браузер завантажує застосунок заново, тому локальний стан скидається.
Якщо одна й та сама навігація потрібна для кількох сторінок розділу, не потрібно дублювати її в кожному page.tsx. Розмістіть її в layout відповідного сегмента.
layout.tsx визначає спільну оболонку для сторінок.
app/layout.tsx є кореневим layout і обгортає весь застосунок.
Layout у вкладеній папці застосовується до всіх маршрутів цієї папки.
Вкладені layouts утворюють дерево відповідно до структури каталогів.
Під час клієнтської навігації через Link layouts залишаються змонтованими.
Локальний стан інтерактивних компонентів у layout зберігається між сторінками одного розділу.
Компоненти зі станом і обробниками подій потрібно робити Client Components.
Для роботи сторінки layout має відображати {children}.