1. Введение
Если вы уже делали хоть что-то на Next.js 15 с App Router (а если нет — самое время начать!), то наверняка замечали: иногда при переходе по страницам часть интерфейса "мигает", иногда — остаётся на месте. Иногда состояние сбрасывается, иногда — сохраняется.
Всё это связано с тем, как Next.js "переиспользует" или "пересоздаёт" layout и template-компоненты при навигации. Понимание этих нюансов позволяет:
- Избегать "мигания" или потери состояния на страницах.
- Грамотно размещать логику и состояние (например, общие меню, корзину, авторизацию).
- Достигать максимальной производительности и плавности навигации.
Если коротко: layout — это про "стабильность", template — про "гибкость". Но давайте разберёмся подробнее!
Напоминаем: что такое layout.tsx и template.tsx?
layout.tsx — это компонент-обёртка для группы маршрутов. Он определяет общую структуру (например, шапку, меню, подвал) для всех вложенных страниц и маршрутов. Layout'ы могут быть вложенными: каждый вложенный layout добавляет свою "обёртку".
template.tsx — это тоже обёртка, но с другим поведением: она создаётся заново при каждом посещении маршрута, к которому относится. Используется для случаев, когда нужно динамически изменять структуру обёртки или сбрасывать состояние при переходах.
Пример структуры
app/
layout.tsx // Главный layout для всего приложения
page.tsx // Главная страница
about/
page.tsx // Страница "О нас"
dashboard/
layout.tsx // Layout для dashboard
template.tsx // Template для dashboard
page.tsx // Главная страница dashboard
analytics/
page.tsx // Вложенная страница analytics
2. Как layout и template ведут себя при навигации
Layout: живёт долго и счастливо
Layout-компоненты в Next.js 15 устроены так, что они сохраняют своё состояние между переходами по вложенным маршрутам. Это значит:
- Если пользователь переходит между страницами, находящимися "под" одним и тем же layout, этот layout не пересоздаётся.
- Любое состояние (например, открытое меню, выбранная вкладка) внутри layout сохраняется, пока пользователь не выйдет из этой области маршрутов.
Иллюстрация
app/
layout.tsx
dashboard/
layout.tsx
page.tsx
analytics/
page.tsx
- Переход с /dashboard на /dashboard/analytics — оба маршрута используют один и тот же layout (dashboard/layout.tsx), он не пересоздаётся.
- Переход с /dashboard/analytics на /about — верхний layout (app/layout.tsx) остаётся, но layout dashboard "уходит".
Почему это круто?
- Не теряется состояние, например, открытые вкладки, фильтры, содержимое корзины и т.д.
- Нет лишних перерисовок — быстрее и плавнее для пользователя.
Template: каждый раз как новый
Template-компоненты ведут себя иначе: они пересоздаются при каждом переходе на новый маршрут в своей области. Это значит:
- Если вы переходите между маршрутами, для которых используется один и тот же template, он будет пересоздан (и его состояние сброшено).
- Это удобно для сброса временного состояния, например, если вы хотите, чтобы при каждом заходе на страницу фильтры или формы были "чистыми".
Иллюстрация
app/
dashboard/
template.tsx
page.tsx
analytics/
page.tsx
Переход с /dashboard на /dashboard/analytics — template пересоздаётся, всё, что было внутри template, сбрасывается.
3. Примеры: как это выглядит на практике
Пример 1: Layout сохраняет состояние
// app/dashboard/layout.tsx
'use client';
import { useState } from "react";
export default function DashboardLayout({ children }) {
const [menuOpen, setMenuOpen] = useState(false);
return (
<div>
<button onClick={() => setMenuOpen(open => !open)}>
{menuOpen ? "Скрыть меню" : "Показать меню"}
</button>
{menuOpen && <nav>Меню</nav>}
<main>{children}</main>
</div>
);
}
Пояснение:
— Откройте /dashboard, нажмите "Показать меню".
— Перейдите на /dashboard/analytics (например, через <Link href="/dashboard/analytics">).
— Меню останется открытым! Layout не пересоздавался.
Пример 2: Template сбрасывает состояние
// app/dashboard/template.tsx
'use client';
import { useState } from "react";
export default function DashboardTemplate({ children }) {
const [counter, setCounter] = useState(0);
return (
<div>
<p>Счётчик: {counter}</p>
<button onClick={() => setCounter(c => c + 1)}>+1</button>
{children}
</div>
);
}
Пояснение:
— Находясь на /dashboard, нажмите "+1" несколько раз.
— Перейдите на /dashboard/analytics.
— Счётчик снова 0! Template пересоздался, состояние сброшено.
4. Полезные нюансы
Когда использовать layout, а когда template?
Layout — для стабильных обёрток и общего состояния
- Вам нужно, чтобы часть интерфейса (меню, шапка, сайдбар) оставалась на месте при навигации по вложенным страницам.
- Вы хотите сохранять состояние между переходами (например, открытые вкладки, фильтры, корзина).
- Требуется общая структура для группы маршрутов.
Template — для сброса состояния и динамических обёрток
- Нужно, чтобы при каждом переходе на страницу состояние сбрасывалось (например, сбросить форму, фильтры).
- Требуется динамически создавать обёртку для каждой страницы (например, уникальные анимации перехода).
- Вы не хотите, чтобы пользователь "тащил" старое состояние между переходами.
Как понять, что layout/template пересоздался?
Иногда хочется "пощупать" разницу. Давайте добавим простой console.log:
// app/dashboard/layout.tsx
console.log('Dashboard layout rendered');
// app/dashboard/template.tsx
console.log('Dashboard template rendered');
Пояснение:
— Переключайтесь между /dashboard и /dashboard/analytics и смотрите в консоль браузера.
— Layout будет выводиться только при первом заходе или уходе из dashboard, а template — каждый раз при переходе между страницами dashboard.
Взаимодействие layout и template: кто кого оборачивает?
Если в одной папке есть и layout.tsx, и template.tsx, то структура вложенности будет такой:
layout.tsx
└─ template.tsx
└─ page.tsx
- Layout всегда "выше" (оборачивает) template.
- Template "заворачивает" только page.tsx (и вложенные маршруты).
Схема
app/
layout.tsx
dashboard/
layout.tsx
template.tsx
page.tsx
analytics/
page.tsx
Переход между /dashboard и /dashboard/analytics:
- app/layout.tsx — не пересоздаётся.
- dashboard/layout.tsx — не пересоздаётся.
- dashboard/template.tsx — пересоздаётся.
- page.tsx — пересоздаётся.
Где лучше хранить состояние?
- Общее состояние (например, авторизация, корзина, глобальные настройки) — в самом верхнем layout (app/layout.tsx).
- Состояние раздела (например, фильтры dashboard) — в layout соответствующего раздела (dashboard/layout.tsx).
- Временное состояние страницы (например, значения формы) — внутри page.tsx или template.tsx.
Если вы случайно поместите важное состояние в template, оно будет теряться при навигации — и вы получите "странные баги", когда корзина вдруг опустела или фильтр сбросился.
5. Типичные ошибки при работе с layout и template
Ошибка №1: Сброс состояния из-за использования template вместо layout.
Новички часто помещают состояние (например, фильтры или открытое меню) в template, ожидая, что оно сохранится при переходах по разделу. В результате при каждом переходе состояние сбрасывается, что раздражает пользователей: "Почему мой фильтр постоянно сбрасывается?!". Если хотите сохранить состояние — используйте layout.
Ошибка №2: "Мигание" интерфейса из-за частых пересозданий.
Если вы используете template там, где нужен layout, интерфейс может "мигать" — например, шапка или меню исчезают и появляются заново при каждом переходе. Это снижает плавность и UX.
Ошибка №3: Неожиданное поведение при глубокой вложенности.
Если у вас много уровней layout и template, легко запутаться, какой компонент когда пересоздаётся. Следите за структурой и используйте консоль для отладки.
Ошибка №4: Размещение тяжёлых вычислений или запросов в template.
Поскольку template пересоздаётся часто, тяжёлые операции внутри него будут выполняться каждый раз при переходе, что может замедлить приложение.
Ошибка №5: Попытка хранить глобальное состояние в template.
Template предназначен для "одноразовых" обёрток. Если вам нужно глобальное или разделяемое состояние — используйте layout или глобальные менеджеры состояния (например, React Context).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ