У промислових вебзастосунках сторінки поділяють спільні структурні каркаси: навігаційні панелі, бічні меню (sidebars), віджети профілю, глобальні аудіоплеєри та футери. У традиційних клієнтських клієнтських SPA під час переходу між сторінками розробникам доводилося долати системні складнощі:
Next.js App Router вирішує ці завдання на фундаментальному рівні через суворий розподіл обов'язків між Layouts (макетами) та Templates (шаблонами).
Layout зберігає життя своєму екземпляру: під час переходів між дочірніми сторінками він ніколи не розмонтовується, зберігає фокус введення, локальний React-стан і не повторює серверні запити.
Template перестворюється з нуля: при кожній зміні URL старий екземпляр повністю знищується (unmount), скидаючи стан і заново запускаючи CSS-анімації та ефекти життєвого циклу.
В архітектурі React Fiber макет (layout.tsx) є стабільним вузлом віртуального дерева, чий локальний стан, DOM-вузли та фокус введення персистентні. Дочірні маршрути передаються в макет виключно через слот children. Коли змінюється вкладений URL, React виконує звірку піддерева (Subtree Reconciliation): оновлюється лише вміст children, тоді як вузол макета залишається недоторканим.
Ієрархія макетів суворо повторює структуру каталогів:
template.tsx) компілюються Next.js як безпосередні дочірні елементи макета відповідного сегмента, але огортаються в динамічний ключ key={currentSegmentId}, викликаючи повне перестворення вузла під час навігації.Клікайте по маршрутах та спостерігайте ізоляцію Preserved State у макетах
Перемикайте маршрути: цей текст не скинеться!
Перехід усередині дашборду: DashboardLayout НЕ перемонтовується! Введений пошуковий текст зберігається.
Процес обробки макетів і шаблонів під час переходу між сторінками регламентований фазами Fiber Reconciliation:
Користувач клікає по посиланню переходу з /dashboard/orders на /dashboard/analytics.
React зіставляє сегменти і визначає, що app/dashboard/layout.tsx є спільним предком. Його інстанс зберігається у Fiber-дереві.
Next.js змінює ключ огортки Template (key='analytics'). Старий вузол template демонтується (unmount), скидаючи стан.
Створюється свіжий екземпляр Template та дочірньої Page. Запускаються ефекти входу та CSS-анімації появи контенту.
Нове дерево впроваджується в слот children батьківського макета без перемальовування навігаційного каркаса.
У сайдбарі app/dashboard/layout.tsx користувач увів пошуковий запит у локальний input, після чого клікнув на посилання переходу між дочірніми сторінками. Що відбудеться зі значенням input?
Кореневий макет створюється в app/layout.tsx і є обов'язковим для будь-якого застосунку Next.js. Лише він може містити кореневі теги <html> та <body>.
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: {
template: '%s | Fullstack Agentic Lab',
default: 'Fullstack Agentic Lab',
},
description: 'Інженерна платформа з Fullstack та AI розробки',
};
interface RootLayoutProps {
children: React.ReactNode;
}
export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="uk" className="dark">
<body className="min-h-screen bg-bg-base text-text-primary antialiased">
<header className="border-b border-border p-4">Глобальна навігація</header>
<main>{children}</main>
</body>
</html>
);
}Починаючи з Next.js 15, якщо макет сегмента розташований усередині динамічного шляху app/[tenant]/layout.tsx, параметр params передається до нього як Promise:
interface TenantLayoutProps {
children: React.ReactNode;
params: Promise<{ tenant: string }>;
}
export default async function TenantLayout({ children, params }: TenantLayoutProps) {
// Асинхронне розв'язання параметрів шляху в макеті
const { tenant } = await params;
return (
<div className="tenant-container flex min-h-screen">
<aside className="tenant-sidebar w-64 border-r border-border p-4">
<h2 className="text-lg font-bold">Організація: {tenant}</h2>
</aside>
<section className="tenant-content flex-1 p-6">
{children}
</section>
</div>
);
}На відміну від page.tsx, серверні компоненти layout.tsx не отримують проп searchParams.
Це фундаментальне архітектурне рішення для забезпечення ефективності кешування:
?page=2&sort=asc), сервер був би змушений перезапитувати та перемальовувати весь батьківський каркас при кожній зміні пагінації чи фільтрів.searchParams дозволяє кешувати його в пам'яті браузера (Client Router Cache) на тривалий час у межах поточної сесії.Якщо вкладеному клієнтському компоненту всередині макета все ж потрібен доступ до рядка запиту, використовуйте клієнтський хук useSearchParams() з next/navigation.
Next.js компілює файл template.tsx як безпосередню внутрішню огортку сторінки всередині слота children макета:
<Layout>
{/* Template огортається в динамічний ключ поточного сегмента */}
<Template key={currentSegmentId}>
<ErrorBoundary fallback={<Error />}>
<Suspense fallback={<Loading />}>
<Page />
</Suspense>
</ErrorBoundary>
</Template>
</Layout>Коли currentSegmentId змінюється з orders на analytics, зміна атрибута key повідомляє рушію React Fiber: «Знищ старе дерево DOM і змонтуй компонент з нуля».
Перед вами завдання: реалізувати CSS-анімацію плавного входження екрана (fade-in) при кожному переході між розділами кабінету, а також надсилати подію аналітики page_view. Який файл слід використати?
'use client';
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
interface TemplateProps {
children: React.ReactNode;
}
export default function DashboardTemplate({ children }: TemplateProps) {
const pathname = usePathname();
useEffect(() => {
// Гарантований запуск аналітики при кожному переході
console.log(`[Telemetry] Page view registered: ${pathname}`);
}, [pathname]);
return (
<div className="animate-in fade-in duration-300">
{children}
</div>
);
}Часте завдання — впровадження клієнтського React Context (тема оформлення, локалізація) в кореневий серверний макет без перетворення всього макета на клієнтський компонент:
'use client';
import { createContext, useContext, useState } from 'react';
type Theme = 'dark' | 'light';
const ThemeContext = createContext<{
theme: Theme;
toggleTheme: () => void;
}>({
theme: 'dark',
toggleTheme: () => {},
});
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('dark');
const toggleTheme = () => {
setTheme((prev) => (prev === 'dark' ? 'light' : 'dark'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export const useTheme = () => useContext(ThemeContext);import { ThemeProvider } from '@/components/providers/ThemeProvider';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="uk">
<body>
{/* ThemeProvider є клієнтом, але children передається із сервера у вигляді RSC без втрати продуктивності! */}
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
);
}import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
interface AdminLayoutProps {
children: React.ReactNode;
}
export default async function AdminLayout({ children }: AdminLayoutProps) {
// Асинхронне читання cookies у Next.js 15+
const cookieStore = await cookies();
const sessionToken = cookieStore.get('auth_token')?.value;
if (!sessionToken) {
redirect('/login?from=/admin');
}
return (
<div className="admin-shell min-h-screen bg-bg-surface">
<header className="border-b border-border p-4 font-bold">
Панель адміністратора
</header>
<main className="p-6">{children}</main>
</div>
);
}Розробники часто намагаються отримати параметри рядка запиту через props.searchParams усередині серверного layout.tsx і стикаються з тим, що значення завжди дорівнює undefined.
Симптом: Спроба розібрати фільтри або пагінацію в макеті призводить до порожнього значення.
Причина: Серверні макети ізольовані від searchParams для довгострокового кешування в Router Cache.
Рішення: Читайте параметри рядка запиту в серверному page.tsx або використовуйте клієнтський хук useSearchParams() в ізольованому клієнтському віджеті всередині макета.
Додавання директиви 'use client' в app/layout.tsx перетворює все компонентне дерево проєкту нижче за ієрархією на Client Components, блокуючи серверний стрімінг даних і роздуваючи бандл.
Симптом: Серверні функції перестають працювати в дочірніх файлах, розмір JS-бандла зростає в рази.
Причина: Директива 'use client' створює межу серіалізації, каскадно транслюючи всі дочірні імпорти в клієнтський бандл.
Рішення: Кореневий макет завжди повинен залишатися Server Component. Клієнтські провайдери виносьте в окремі компоненти і передавайте серверні сторінки через проп {children}.
На відміну від Pages Router, де базові теги сторінки генерувалися приховано в _document.tsx, в App Router кореневий макет app/layout.tsx зобов'язаний явно оголошувати ці елементи.
Симптом: Фатальний збій компіляції Turbopack: «Root layout must contain <html> and <body> tags».
Рішення: Переконайтеся, що найвищий макет повертає повну базову HTML-структуру.