Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Макети та шаблони в Next.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Макети та шаблони в Next.js** обгортають дочірні сторінки і визначають спільний UI. Макет зберігається між переходами (стан виживає), шаблон повністю перестворюється при кожній зміні маршруту (стан скидається). ```tsx // layout.tsx - useState виживає при /a -> /b // template.tsx - ремонтується і скидається при /a -> /b ``` **Ключове:** використовуй макет за замовчуванням; шаблон - лише коли потрібен гарантований remount при кожному відвідуванні.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Макети (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>` змінюється без будь-якого впливу на сайдбар.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.