4. Layouts и Templates
🏗️ Layouts и Templates в Next.js
Заголовок раздела «🏗️ Layouts и Templates в Next.js»Layout — это компонент-обёртка, который сохраняется между навигациями. Это суперсила App Router: ты пишешь навигацию, сайдбар, хедер один раз — и он не перерендеривается при переходах между страницами. Экономия ресурсов и плавный UX! ✨
Навигация /dashboard → /dashboard/settings
Pages Router: DashboardLayout РАЗМОНТИРУЕТСЯ и МОНТИРУЕТСЯ заново 😢App Router: DashboardLayout ОСТАЁТСЯ, обновляется только children 🎉📐 Root Layout: Фундамент всего приложения
Заголовок раздела «📐 Root Layout: Фундамент всего приложения»Root layout — единственный обязательный файл в App Router. Он должен содержать теги <html> и <body>:
// app/layout.tsx — обязательный root layoutimport type { Metadata } from 'next';import { Inter } from 'next/font/google';import './globals.css';
const inter = Inter({ subsets: ['latin', 'cyrillic'] });
// Метаданные для всего сайта (наследуются дочерними)export const metadata: Metadata = { title: { template: '%s | Мой сайт', // %s — заголовок страницы default: 'Мой сайт', // если страница не указала title }, description: 'Описание сайта', metadataBase: new URL('https://mysite.com'), openGraph: { type: 'website', locale: 'ru_RU', },};
export default function RootLayout({ children,}: { children: React.ReactNode;}) { return ( <html lang="ru" suppressHydrationWarning> <body className={inter.className}> <ThemeProvider> <Header /> {children} <Footer /> </ThemeProvider> </body> </html> );}Что обязательно в Root Layout:
- Теги
<html>и<body>(только в root!) - Может быть
async(Server Component) - Должен принимать
children
🪆 Вложенные Layouts
Заголовок раздела «🪆 Вложенные Layouts»Каждый сегмент маршрута может иметь свой layout. Они вкладываются друг в друга:
app/├── layout.tsx ← Root: Header + Footer для всего сайта├── page.tsx → /├── blog/│ ├── layout.tsx ← Blog: ширина контента, теги│ ├── page.tsx → /blog│ └── [slug]/│ └── page.tsx → /blog/:slug└── dashboard/ ├── layout.tsx ← Dashboard: сайдбар ├── page.tsx → /dashboard └── settings/ ├── layout.tsx ← Settings: вкладки настроек └── page.tsx → /dashboard/settingsimport { DashboardSidebar } from '@/components/DashboardSidebar';import { getCurrentUser } from '@/lib/auth';import { redirect } from 'next/navigation';
export default async function DashboardLayout({ children,}: { children: React.ReactNode;}) { // Server Component — можем делать запросы к БД! const user = await getCurrentUser();
// Защита роутов прямо в layout if (!user) { redirect('/login'); }
return ( <div style={{ display: 'flex', minHeight: '100vh' }}> <DashboardSidebar user={user} /> <main style={{ flex: 1, padding: '24px' }}> {children} </main> </div> );}export default function SettingsLayout({ children,}: { children: React.ReactNode;}) { return ( <div> <h1>Настройки</h1> <nav> <Link href="/dashboard/settings">Профиль</Link> <Link href="/dashboard/settings/security">Безопасность</Link> <Link href="/dashboard/settings/billing">Оплата</Link> </nav> <div className="settings-content">{children}</div> </div> );}Итоговое дерево для /dashboard/settings:
<RootLayout> {/* app/layout.tsx */} <DashboardLayout> {/* app/dashboard/layout.tsx */} <SettingsLayout> {/* app/dashboard/settings/layout.tsx */} <SettingsPage> {/* app/dashboard/settings/page.tsx */} </SettingsPage> </SettingsLayout> </DashboardLayout></RootLayout>🔄 template.tsx vs layout.tsx
Заголовок раздела «🔄 template.tsx vs layout.tsx»Это тонкое, но важное различие:
// layout.tsx — ПЕРСИСТЕНТНЫЙ: сохраняется, НЕ перемонтируетсяexport default function Layout({ children }) { const [count, setCount] = useState(0); // Сохраняется между переходами! return ( <div> <p>Счётчик: {count}</p> {/* Не сбросится при навигации */} {children} </div> );}
// template.tsx — ЭФЕМЕРНЫЙ: перемонтируется при каждой навигацииexport default function Template({ children }) { const [count, setCount] = useState(0); // Сбрасывается при каждом переходе! return ( <div> <p>Счётчик: {count}</p> {/* Сбросится при переходе на другую страницу */} {children} </div> );}Когда использовать template.tsx:
// 1. Анимации входа/выхода страниц'use client';
import { motion } from 'framer-motion';
export default function Template({ children }) { return ( <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -20 }} transition={{ duration: 0.3 }} > {children} </motion.div> );}// Анимация будет воспроизводиться при КАЖДОМ переходе!
// 2. Трекинг просмотров страниц// app/template.tsx'use client';import { useEffect } from 'react';import { usePathname } from 'next/navigation';
export default function Template({ children }) { const pathname = usePathname();
useEffect(() => { // Выполнится при каждом переходе на новую страницу analytics.track('page_view', { path: pathname }); }, [pathname]);
return <>{children}</>;}📋 Метаданные в Layouts
Заголовок раздела «📋 Метаданные в Layouts»Next.js предоставляет два способа задать метаданные:
// 1. Статические метаданныеimport type { Metadata } from 'next';
export const metadata: Metadata = { title: 'Блог', description: 'Статьи о программировании', keywords: ['Next.js', 'React', 'TypeScript'], openGraph: { title: 'Блог | Мой сайт', description: 'Статьи о программировании', url: 'https://mysite.com/blog', images: [ { url: '/og-blog.jpg', width: 1200, height: 630, alt: 'Блог', }, ], }, twitter: { card: 'summary_large_image', title: 'Блог | Мой сайт', description: 'Статьи о программировании', },};
export default function BlogLayout({ children }) { return <div className="blog-layout">{children}</div>;}// 2. Динамические метаданные (для страниц с данными)import type { Metadata } from 'next';
interface Props { params: Promise<{ slug: string }>;}
// generateMetadata — async функция для динамических метаданныхexport async function generateMetadata({ params }: Props): Promise<Metadata> { const { slug } = await params; const post = await getPost(slug);
if (!post) { return { title: 'Пост не найден' }; }
return { title: post.title, description: post.excerpt, openGraph: { title: post.title, description: post.excerpt, images: [post.coverImage], type: 'article', publishedTime: post.publishedAt, authors: [post.author.name], }, };}
export default async function BlogPost({ params }: Props) { const { slug } = await params; const post = await getPost(slug); return <article>{post.content}</article>;}Наследование и слияние метаданных:
export const metadata = { title: { template: '%s | MySite', default: 'MySite' }, description: 'Лучший сайт в интернете',};
// app/blog/layout.tsxexport const metadata = { // title не указан → возьмёт из родителя template description: 'Статьи о коде', // Переопределяет родительский};
// app/blog/[slug]/page.tsxexport const metadata = { title: 'Мой пост', // Итог: 'Мой пост | MySite' // description не указан → 'Статьи о коде'};🖥️ Server vs Client Layouts
Заголовок раздела «🖥️ Server vs Client Layouts»Layouts по умолчанию — Server Components. Это значит ты можешь делать запросы к БД прямо в них:
// app/dashboard/layout.tsx — Server Component (по умолчанию)import { db } from '@/lib/db';import { getSession } from '@/lib/auth';
export default async function DashboardLayout({ children }) { // Прямой запрос к базе данных! const session = await getSession(); const notifications = await db.notification.findMany({ where: { userId: session.user.id, read: false }, orderBy: { createdAt: 'desc' }, take: 5, });
return ( <div> <DashboardHeader user={session.user} notificationCount={notifications.length} /> <main>{children}</main> </div> );}// app/dashboard/layout.tsx — Client Component (если нужны hooks/events)'use client';
import { useState, useEffect } from 'react';
export default function DashboardLayout({ children }) { const [sidebarOpen, setSidebarOpen] = useState(true);
// localStorage, window, DOM useEffect(() => { const saved = localStorage.getItem('sidebar'); if (saved) setSidebarOpen(saved === 'true'); }, []);
return ( <div> <button onClick={() => setSidebarOpen(!sidebarOpen)}> {sidebarOpen ? '◀' : '▶'} </button> {sidebarOpen && <Sidebar />} <main>{children}</main> </div> );}💡 Лучшие практики
Заголовок раздела «💡 Лучшие практики»// 1. Держи Root Layout минимальным и серверным// app/layout.tsx — ХОРОШОexport default function RootLayout({ children }) { return ( <html lang="ru"> <body> {children} {/* Минимум логики */} </body> </html> );}
// 2. Используй вложенные layouts для изоляции// Плохо: один жирный layout для всего// Хорошо: отдельные layouts для разных секций
// 3. Server Components по умолчанию// Добавляй 'use client' только когда нужны:// - useState, useEffect, useRef// - Event handlers// - Browser APIs (window, localStorage)// - Third-party клиентские библиотеки
// 4. Параметры из URL в layout// app/dashboard/[userId]/layout.tsxexport default async function UserLayout({ children, params}: { children: React.ReactNode; params: Promise<{ userId: string }>;}) { const { userId } = await params; const user = await getUser(userId);
return ( <UserProvider user={user}> {children} </UserProvider> );}🎯 Резюме урока
Заголовок раздела «🎯 Резюме урока»- Root Layout — обязательный, содержит
<html>и<body> - Вложенные Layouts — создаются в подпапках, вкладываются автоматически
- template.tsx — как layout, но перемонтируется при каждой навигации
- Метаданные —
export const metadataилиgenerateMetadata()async - Server by default — layouts серверные, добавляй
'use client'только если нужно