У класичних веб-застосунках перехід за посиланнями призводив до повного руйнування поточної сторінки: браузер заново запитував 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="uk" 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>!