В промышленных веб-приложениях страницы разделяют общие структурные каркасы: навигационные бары, боковые панели (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="ru" 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 16, если макет сегмента расположен внутри динамического пути 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="ru">
<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 16
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-структуру.