Макети та шаблони в Next.js
Макети (layouts) та шаблони (templates) в Next.js App Router - це обгортки для дочірніх сторінок. Різниця в одному імені файлу і великій різниці в поведінці: макет залишається змонтованим між переходами, шаблон перестворюється кожного разу.
Теорія
TL;DR
- Макет = стіни будинку (стоять поки кімнати змінюються); шаблон = намет (розбирається при кожному переїзді)
- Головна різниця: макети зберігають
useStateі не перерендерюються; шаблони скидають все - Використовуй макети для навбарів, сайдбарів та auth-провайдерів (99% випадків)
- Шаблони - тільки коли потрібен гарантований remount при кожному відвідуванні: анімації, логування переглядів, скидання форм
- Кореневий
layout.tsxє обов'язковим і повинен містити<html>та<body>
Швидкий приклад
// app/layout.tsx - стан зберігається між переходами
'use client';
import { useState } from 'react';
export default function Layout({ children }: { children: React.ReactNode }) {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Лічильник: {count}</button>
{/* /a -> /b: count залишається тим самим */}
{children}
</div>
);
}
// app/template.tsx - перестворюється при кожному переході
export default function Template({ children }: { children: React.ReactNode }) {
// /a -> /b: повний remount, count скидається до 0
return <div>{children}</div>;
}Перехід з /a на /b: макет зберігає значення лічильника. Шаблон скидається до нуля.
Ключова різниця
Макет рендериться один раз для кожного сегмента маршруту і компонується з children без remount. React розглядає макет як стабільний вузол дерева компонентів, тому useState, useEffect і DOM-вузли виживають при переходах. Шаблон повністю демонтується і монтується знову при кожній зміні маршруту. React reconciler бачить новий екземпляр при кожному переході, що запускає нові виклики useEffect і скидає весь локальний стан.
Коли що використовувати
- Спільний навбар або сайдбар зі станом (відкриті меню, активний таб) → макет
- Логування переглядів при кожному відвідуванні (
useEffectмає спрацьовувати щоразу) → шаблон - Анімації входу/виходу, що залежать від lifecycle монтування → шаблон
- Вкладені сегменти з різним контекстом (
/dashboardі/settings) → вкладений макет - Auth-провайдер для всього застосунку → макет (сесія виживає між переходами)
- Кореневий
<html>/<body>wrapper → макет (обов'язково за вимогою Next.js)
Таблиця порівняння
| Властивість | Макет | Шаблон |
|---|---|---|
| Remount при переході | Ні | Так, кожного разу |
useState | Зберігається | Скидається |
useEffect | Не запускається знову | Запускається знову |
| DOM | Не відтворюється | Відтворюється |
| Вкладеність | Компонується вгору по дереву | Підтримується, скидається при кожному переході |
| Типове використання | Навбари, сайдбари, провайдери | Анімації, логування переглядів |
Порядок вкладення всередині сегмента
Next.js обгортає спеціальні файли у фіксованому порядку для кожного сегмента:
layout.tsx
template.tsx
error.tsx (React Error Boundary)
loading.tsx (React Suspense)
not-found.tsx
page.tsxЦей порядок важливий, коли ти комбінуєш template.tsx і loading.tsx в одному сегменті.
Як це працює
Next.js сканує директорію app/ під час збірки і в режимі розробки, розглядаючи layout.tsx і template.tsx як спеціальні RSC-межі. При зміні маршруту React reconciler перевіряє, чи є компонент макету для поточного сегмента тим самим екземпляром. Так, тому демонтування не відбувається. Для шаблонів reconciler бачить новий екземпляр при кожному переході і виконує повний teardown і remount. Макети стримуються до браузера першими, одразу малюючи оболонку застосунку; вміст сторінок заповнюється після.
Типові помилки
Стан авторизації в шаблоні:
// app/template.tsx - НЕПРАВИЛЬНО
'use client';
const [user] = useSession(); // Скидається при кожному переході, спричиняє цикли входуШаблони ремонтуються при кожній зміні маршруту, вбиваючи стан хука. Логіка сесії має бути в макеті.
Забутий {children} в макеті:
// app/docs/layout.tsx - НЕПРАВИЛЬНО
export default function DocsLayout() {
return <div>Заголовок Docs</div>; // Вміст сторінки зникає
}Кожен макет повинен рендерити {children}. Без цього дочірні сторінки просто не відображаються.
Шаблон для навбару. Навбар буде мерехтіти і перерендерюватися при кожному кліку на посилання. Це найпоширеніша помилка в App Router, яку я бачу в реальних кодових базах. Макети існують саме для того, щоб цього уникнути.
Кореневий макет без <html> та <body>:
// app/layout.tsx - НЕПРАВИЛЬНО
export default function RootLayout({ children }) {
return <div>{children}</div>; // Гідрація ламається
}Next.js вимагає <html> та <body> в кореневому макеті для роботи SSR і стрімінгу.
Де зустрічається
- Vercel Dashboard: кореневий + вкладені макети зберігають стан сайдбара і перемикача організацій між переходами
- Supabase + Next.js стартери: макет обгортає auth-провайдер, щоб сесія виживала між сторінками
- T3 Stack (tRPC/Next): вкладені макети передають контекст користувача та команди вниз по дереву
- Shadcn/ui шаблони: оболонка застосунку в макеті, рідкісні onboarding-екрани в шаблоні
- Будь-який дашборд з табами: макет зі
useState('analytics'), що зберігається від/dashboard/analyticsдо/dashboard/reports
Питання для співбесіди
Q: Як компонуються вкладені макети? Як виглядає дерево для app/blog/layout.tsx і app/blog/posts/layout.tsx?
A: Кореневий макет обгортає blog-макет, який обгортає posts-макет. Всі три рендеряться один раз при завантаженні; тільки найвнутрішній компонент сторінки змінюється при переходах. Батьківські макети не ремонтуються.
Q: Навіщо використовувати template.tsx замість loading.tsx для кастомного UI скидання?
A: loading.tsx автоматично обгортає сегмент у Suspense-межу і показує спінер під час завантаження даних. template.tsx відповідає за повну логіку remount: скидання стану, анімації, ефекти при кожному перегляді. Вони вирішують різні задачі.
Q: Коли б ти використав шаблон для динамічної сторінки товару?
A: Якщо потрібні свіжі дані при кожному відвідуванні без кешування, шаблон всередині динамічного маршруту (/shop/[id]/template.tsx) з cache: 'no-store' гарантує повторний запит щоразу, коли користувач відкриває цей товар.
Q: (Senior) Як працює стрімінг макетів разом із Suspense-межами в дочірніх компонентах?
A: Макет стримується до браузера першим як HTML-оболонка. Дочірні компоненти з асинхронними запитами зупиняються незалежно. Браузер миттєво малює оболонку; вміст заповнюється по мірі того, як кожна Suspense-межа розв'язується. Водоспаду немає, бо макет не блокує стрімінг дочірніх компонентів.
Приклади
Базовий: Кореневий макет зі спільними заголовком і підвалом
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="uk">
<body>
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
);
}
// Header і Footer монтуються один раз; тільки вміст <main> змінюється при переходахHeader і Footer монтуються один раз на весь час роботи застосунку. Переходи між сторінками їх не перестворюють і не скидають їхній стан.
Середній рівень: Дашборд зі збереженням стану сайдбара
// app/dashboard/layout.tsx
'use client';
import { useState } from 'react';
import Sidebar from '@/components/sidebar';
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const [activeTab, setActiveTab] = useState('analytics');
// /dashboard/analytics -> /dashboard/reports:
// activeTab залишається 'analytics', Sidebar не мерехтить
return (
<div className="flex">
<Sidebar active={activeTab} onChange={setActiveTab} />
<main className="flex-1 p-8">{children}</main>
</div>
);
}activeTab виживає при переходах, бо макет ніколи не демонтується. Вміст сторінки всередині <main> змінюється без будь-якого впливу на сайдбар.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.