Skip to main content

Макети та шаблони в Next.js

Макети (layouts) та шаблони (templates) в Next.js App Router - це обгортки для дочірніх сторінок. Різниця в одному імені файлу і великій різниці в поведінці: макет залишається змонтованим між переходами, шаблон перестворюється кожного разу.

Теорія

TL;DR

  • Макет = стіни будинку (стоять поки кімнати змінюються); шаблон = намет (розбирається при кожному переїзді)
  • Головна різниця: макети зберігають useState і не перерендерюються; шаблони скидають все
  • Використовуй макети для навбарів, сайдбарів та auth-провайдерів (99% випадків)
  • Шаблони - тільки коли потрібен гарантований remount при кожному відвідуванні: анімації, логування переглядів, скидання форм
  • Кореневий layout.tsx є обов'язковим і повинен містити <html> та <body>

Швидкий приклад

tsx
// 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. Макети стримуються до браузера першими, одразу малюючи оболонку застосунку; вміст сторінок заповнюється після.

Типові помилки

Стан авторизації в шаблоні:

tsx
// app/template.tsx - НЕПРАВИЛЬНО 'use client'; const [user] = useSession(); // Скидається при кожному переході, спричиняє цикли входу

Шаблони ремонтуються при кожній зміні маршруту, вбиваючи стан хука. Логіка сесії має бути в макеті.

Забутий {children} в макеті:

tsx
// app/docs/layout.tsx - НЕПРАВИЛЬНО export default function DocsLayout() { return <div>Заголовок Docs</div>; // Вміст сторінки зникає }

Кожен макет повинен рендерити {children}. Без цього дочірні сторінки просто не відображаються.

Шаблон для навбару. Навбар буде мерехтіти і перерендерюватися при кожному кліку на посилання. Це найпоширеніша помилка в App Router, яку я бачу в реальних кодових базах. Макети існують саме для того, щоб цього уникнути.

Кореневий макет без <html> та <body>:

tsx
// 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-межа розв'язується. Водоспаду немає, бо макет не блокує стрімінг дочірніх компонентів.

Приклади

Базовий: Кореневий макет зі спільними заголовком і підвалом

tsx
// 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 монтуються один раз на весь час роботи застосунку. Переходи між сторінками їх не перестворюють і не скидають їхній стан.

Середній рівень: Дашборд зі збереженням стану сайдбара

tsx
// 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> змінюється без будь-якого впливу на сайдбар.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?