В классических веб-приложениях переход по ссылкам приводил к полному разрушению текущей страницы: браузер заново запрашивал HTML, парсил CSS, скачивал скрипты и сбрасывал состояние всех форм, скролла и открытых меню.
Next.js 16 App Router решает эту фундаментальную проблему с помощью двух взаимосвязанных механизмов:
layout.tsx): общие оболочки интерфейса (Header, Sidebar, Navigation), которые не перерендериваются и сохраняют свое состояние (Preserved State) при переходе между дочерними страницами (Subtree Reconciliation). При клике на ссылку перерисовывается исключительно листовой компонент page.tsx.<Link>: компонент навигации, реализующий «мягкие переходы» (Soft Navigation) без перезагрузки вкладки. Компонент автоматически выполняет предзагрузку (Prefetching) кода и данных целевого маршрута в фоновом режиме, как только ссылка попадает в область видимости экрана (Viewport).Взглянем на то, как устроен рендеринг при переходе между страницами:
+-------------------------------------------------------------------------+
| СОХРАНЕНИЕ СОСТОЯНИЯ ВО ВЛОЖЕННЫХ ЛЕЙАУТАХ |
+-------------------------------------------------------------------------+
| [RootLayout: app/layout.tsx] -----------------------------------------+ |
| | Шапка сайта, Корзина, Тема (НЕ ПЕРЕРЕНДЕРИВАЮТСЯ НИКОГДА) | |
| | | |
| | [CatalogLayout: app/catalog/layout.tsx] --------------------------+ | |
| | | Боковые фильтры, Сайдбар (СОХРАНЯЮТ СВОЙ ВНУТРЕННИЙ СТЕЙТ!) | | |
| | | | | |
| | | При переходе с /catalog/phones на /catalog/laptops: | | |
| | | МЕНЯЕТСЯ ТОЛЬКО ЛИСТОВОЙ КОМПОНЕНТ: | | |
| | | Было: [Page: PhonesPage] | | |
| | | Стало: [Page: LaptopsPage] | | |
| | +-----------------------------------------------------------------+ | |
| +---------------------------------------------------------------------+ |
+-------------------------------------------------------------------------+IntersectionObserver браузера фиксирует появление компонента <Link> на экране.
Next.js тихо скачивает серверный Flight-пейлоад целевого маршрута в кэш роутера (Router Cache).
Обработчик onClick перехватывает событие (e.preventDefault()) и обращается к готовому кэшу.
Родительский layout.tsx сохраняет свой DOM и стейт, а новый page.tsx монтируется мгновенно.
Пользователь ввел текст в поисковый инпут, расположенный внутри `app/catalog/layout.tsx`, и кликнул на ссылку, ведущую на другую категорию каталога. Что произойдет со введенным текстом в инпуте?
app/layout.tsx с подключением шрифтов (next/font), глобальных стилей и тегов <html> и <body>.template.tsx вместо layout.tsx, когда требуется перезапуск CSS-анимаций на каждый переход.app/layout.tsx#Корневой лейаут обязателен для любого проекта на Next.js. Он обязан содержать теги <html> и <body>:
import type { Metadata } from 'next';
import Link from 'next/link';
import '@/app/globals.css';
export const metadata: Metadata = {
title: 'WPSKILLS Platform',
description: 'Инженерная обучающая платформа нового поколения',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ru" className="dark">
<body className="bg-bg-base text-text-primary min-h-screen flex flex-col antialiased">
{/* Глобальная шапка (сохраняется на всех страницах) */}
<header className="border-b border-border p-4 bg-bg-surface/80 backdrop-blur-md sticky top-0 z-50">
<nav className="max-w-6xl mx-auto flex items-center justify-between">
<Link href="/" className="font-bold text-accent-cyan text-lg">
WPSKILLS
</Link>
<div className="flex gap-4 text-sm font-medium">
<Link href="/catalog" className="hover:text-accent-cyan transition-colors">
Каталог
</Link>
<Link href="/dashboard" className="hover:text-accent-cyan transition-colors">
Личный кабинет
</Link>
</div>
</nav>
</header>
{/* Сюда подставляются дочерние страницы и подлейауты */}
<main className="flex-1">{children}</main>
<footer className="border-t border-border p-6 text-center text-xs text-text-muted">
© 2026 WPSKILLS. Все права защищены.
</footer>
</body>
</html>
);
}<Link>#Компонент <Link> из next/link заменяет стандартный HTML-тег <a>:
import Link from 'next/link';
export function NavigationCards() {
return (
<div className="grid grid-cols-2 gap-4">
{/* Мягкий переход с автоматическим prefetching по умолчанию */}
<Link
href="/catalog/typescript"
className="p-6 rounded-xl border border-border bg-bg-surface hover:border-accent-cyan transition-all"
>
<h3 className="font-bold">TypeScript Mastery →</h3>
<p className="text-sm text-text-muted mt-1">Освойте строгую типизацию</p>
</Link>
{/* Отключение prefetch для редко посещаемых или тяжелых страниц */}
<Link
href="/admin/heavy-reports"
prefetch={false}
className="p-6 rounded-xl border border-border bg-bg-surface hover:border-accent-violet transition-all"
>
<h3 className="font-bold">Отчеты (No Prefetch) →</h3>
</Link>
</div>
);
}layout.tsx и template.tsx#Если вам нужно, чтобы компонент перемонтировался заново на каждый переход (например, для запуска входной анимации или сброса внутреннего состояния формы), используйте файл template.tsx:
'use client';
import React from 'react';
// template.tsx создает НОВЫЙ экземпляр DOM-узла на каждый переход!
export default function CatalogTemplate({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="animate-in fade-in duration-300">
{children}
</div>
);
}В каком случае разработчик обязан выбрать template.tsx вместо layout.tsx?
Теги <html> и <body> разрешены строго в единственном экземпляре — в корневом app/layout.tsx. Попытка добавить их во вложенный app/catalog/layout.tsx приведет к синтаксическому конфликту DOM и фатальной ошибке гидратации.
Часто новички пишут <div onClick={() => router.push('/path')}>.
Почему это плохо:
href в DOM (удар по SEO);<Link>!