Пошук уроків, статей та іншого контенту
Відображайте маршрут поверх поточного UI та повертайтеся до повної сторінки за допомогою Intercepting Routes.
Intercepting Routes у Next.js App Router дають змогу перехопити навігацію до маршруту та показати його поверх поточного інтерфейсу.
Найпоширеніший сценарій — модальне вікно:
Користувач знаходиться на сторінці списку.
Натискає посилання на окремий ресурс.
Next.js показує цей ресурс у модальному вікні поверх списку.
Поточний UI залишається змонтованим під модальним вікном.
Натискання кнопки «Назад» закриває модальне вікно.
Якщо відкрити URL безпосередньо або оновити сторінку, ресурс відображається як повна сторінка.
Це відрізняється від звичайної навігації, під час якої весь вміст поточного маршруту замінюється новою сторінкою.
Спеціальні назви папок визначають, який маршрут потрібно перехопити:
(.) — маршрут на тому самому рівні;
(..) — маршрут на один рівень вище;
(..)(..) — маршрут на два рівні вище;
(...) — маршрут від кореня app.
Назва папки з такими дужками не потрапляє до URL. Вона використовується лише Next.js для визначення маршруту перехоплення.
Наприклад:
app/
├── photos/
│ └── [id]/
│ └── page.tsx
└── @modal/
└── (.)photos/
└── [id]/
└── page.tsxПапка @modal є parallel route slot, а (.)photos означає: перехопити маршрут photos на тому самому рівні.
Для відображення модального вікна потрібні:
звичайна сторінка маршруту;
перехоплена версія маршруту;
slot, наприклад @modal;
layout, який відображає цей slot;
default.tsx для slot за замовчуванням.
Розглянемо структуру застосунку:
app/
├── @modal/
│ ├── (.)photos/
│ │ └── [id]/
│ │ └── page.tsx
│ └── default.tsx
├── photos/
│ ├── [id]/
│ │ └── page.tsx
│ └── page.tsx
├── layout.tsx
└── page.tsx
components/
├── PhotoDetails.tsx
└── PhotoModal.tsxLayout отримує slot як додатковий проп. Назва пропу відповідає назві папки @modal.
// app/layout.tsx
import type { ReactNode } from "react";
import "./globals.css";
export default function RootLayout({
children,
modal,
}: {
children: ReactNode;
modal: ReactNode;
}) {
return (
<html lang="uk">
<body>
{children}
{modal}
</body>
</html>
);
}children містить основний маршрут, а modal — вміст parallel route slot.
Файл default.tsx повертає null, коли slot не має активного маршруту.
// app/@modal/default.tsx
export default function DefaultModal() {
return null;
}Цей файл особливо важливий під час початкового завантаження або прямого відкриття URL.
// app/page.tsx
import Link from "next/link";
const photos = [
{ id: "1", title: "Гори" },
{ id: "2", title: "Море" },
{ id: "3", title: "Ліс" },
];
export default function HomePage() {
return (
<main>
<h1>Фотографії</h1>
<ul>
{photos.map((photo) => (
<li key={photo.id}>
<Link href={`/photos/${photo.id}`}>{photo.title}</Link>
</li>
))}
</ul>
</main>
);
}Користувач переходить за звичайним URL /photos/1. Зовні це звичайне посилання, але під час клієнтської навігації Next.js може перехопити його через slot @modal.
Повна сторінка потрібна для прямого відкриття URL і перезавантаження сторінки.
// app/photos/[id]/page.tsx
import PhotoDetails from "@/components/PhotoDetails";
export default async function PhotoPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return (
<main>
<PhotoDetails id={id} />
</main>
);
}Цей маршрут працює незалежно від механізму перехоплення. Наприклад, він буде використаний, коли користувач:
вставить /photos/1 в адресний рядок;
відкриє URL у новій вкладці;
оновить сторінку;
перейде на URL ззовні застосунку.
Щоб не дублювати розмітку, деталі фотографії можна винести в окремий компонент:
// components/PhotoDetails.tsx
type PhotoDetailsProps = {
id: string;
};
export default function PhotoDetails({ id }: PhotoDetailsProps) {
return (
<article>
<h1>Фотографія {id}</h1>
<p>Детальна інформація про фотографію.</p>
</article>
);
}Цей компонент використовується і для повної сторінки, і для модального представлення.
Для закриття модального вікна використовується router.back(). Він повертає користувача до попереднього стану історії браузера.
// components/PhotoModal.tsx
"use client";
import type { ReactNode, MouseEvent } from "react";
import { useRouter } from "next/navigation";
type PhotoModalProps = {
children: ReactNode;
};
export default function PhotoModal({ children }: PhotoModalProps) {
const router = useRouter();
function closeModal() {
router.back();
}
function handleBackdropClick(event: MouseEvent<HTMLDivElement>) {
if (event.target === event.currentTarget) {
closeModal();
}
}
return (
<div
role="dialog"
aria-modal="true"
onClick={handleBackdropClick}
style={{
position: "fixed",
inset: 0,
display: "grid",
placeItems: "center",
background: "rgb(0 0 0 / 50%)",
padding: "1rem",
}}
>
<div
style={{
position: "relative",
width: "min(100%, 32rem)",
borderRadius: "0.75rem",
background: "white",
padding: "2rem",
}}
>
<button
type="button"
onClick={closeModal}
aria-label="Закрити модальне вікно"
style={{
position: "absolute",
top: "0.75rem",
right: "0.75rem",
}}
>
×
</button>
{children}
</div>
</div>
);
}Це клієнтський компонент, оскільки useRouter доступний лише в Client Components.
Тепер створимо маршрут усередині @modal:
// app/@modal/(.)photos/[id]/page.tsx
import PhotoDetails from "@/components/PhotoDetails";
import PhotoModal from "@/components/PhotoModal";
export default async function InterceptedPhotoPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return (
<PhotoModal>
<PhotoDetails id={id} />
</PhotoModal>
);
}Під час переходу з / на /photos/1 через Link:
основна сторінка залишається в children;
маршрут /photos/1 перехоплюється;
його вміст відображається в modal;
PhotoModal показує деталі поверх поточного UI.
Один і той самий URL може мати різне представлення залежно від способу навігації.
/ → /photos/1Якщо перехід виконується через Link у вже завантаженому Next.js-застосунку, буде показано перехоплений маршрут у модальному вікні.
https://example.com/photos/1У цьому випадку Next.js завантажить звичайний маршрут app/photos/[id]/page.tsx, тому фотографія відобразиться як повна сторінка.
Таке розділення дає змогу одночасно підтримувати:
зручний перегляд у контексті поточного інтерфейсу;
повноцінні URL;
оновлення сторінки;
відкриття маршруту в новій вкладці;
індексацію та доступність окремого маршруту.
Розглянемо узагальнений приклад:
app/
├── feed/
│ └── page.tsx
├── photo/
│ └── [id]/
│ └── page.tsx
└── @modal/
└── (.)photo/
└── [id]/
└── page.tsx(.)photo перехоплює маршрут photo, який знаходиться на тому самому рівні.
Для маршрутів із глибшою структурою можуть використовуватися інші варіанти:
app/
├── dashboard/
│ ├── settings/
│ │ └── page.tsx
│ └── @modal/
│ └── (..)settings/
│ └── page.tsxТут (..)settings піднімається на один рівень у структурі маршрутів і перехоплює settings.
Важливо враховувати саме структуру маршрутів, а не буквальну кількість папок у файловій системі. Route groups у круглих дужках не створюють сегмент URL.
У модальному вікні зазвичай використовується:
router.back();Це краще, ніж переходити на жорстко заданий URL, тому що користувач може відкрити модальне вікно з різних сторінок.
Наприклад:
зі списку фотографій користувач повернеться до списку;
з результатів пошуку — до результатів пошуку;
з іншого фільтрованого представлення — до нього ж.
Кнопка «Назад» браузера працює аналогічно: вона закриває перехоплений маршрут і повертає попередній стан.
Перехоплений маршрут є повноцінним маршрутом Next.js. Для нього можна створити власні файли:
app/@modal/(.)photos/[id]/
├── error.tsx
├── loading.tsx
└── page.tsxЦе дає змогу окремо керувати станами:
завантаженням вмісту модального вікна;
помилкою під час завантаження;
основним вмістом маршруту.
Якщо для повної сторінки та модального вікна потрібна однакова логіка, спільні компоненти й отримання даних краще винести за межі обох page.tsx.
Якщо layout не відображає modal, перехоплений маршрут не буде видно:
export default function Layout({ children, modal }) {
return (
<>
{children}
{modal}
</>
);
}default.tsxДля parallel route slot потрібно передбачити стан за замовчуванням:
// app/@modal/default.tsx
export default function Default() {
return null;
}Без нього можуть виникати проблеми під час початкового завантаження або прямої навігації.
Якщо відкрити URL напряму або оновити сторінку, перехоплений маршрут не повинен замінювати повну сторінку. Це очікувана поведінка.
(.), (..) і (...) визначають різні рівні маршруту. Якщо модальне вікно не відкривається, потрібно перевірити:
фактичну структуру сегментів маршруту;
розташування папки @modal;
назву папки перехоплення;
відповідність динамічних сегментів, наприклад [id].
Такий код може повернути не на ту сторінку:
<Link href="/">Закрити</Link>Для модального сценарію зазвичай краще використовувати:
router.back();Перехоплений маршрут не замінює звичайний маршрут. Потрібно мати обидві версії:
app/photos/[id]/page.tsx
app/@modal/(.)photos/[id]/page.tsxПерша відповідає за повну сторінку, друга — за представлення поверх поточного UI.
Intercepting Routes дають змогу показувати маршрут поверх поточного інтерфейсу.
Найчастіший сценарій — відкриття деталей у модальному вікні.
Для цього використовується parallel route slot, наприклад @modal.
(.) перехоплює маршрут на тому самому рівні, (..) — на один рівень вище, (...) — від кореня.
Потрібно створити і повний маршрут, і його перехоплену версію.
Під час клієнтської навігації відкривається модальне представлення.
Під час прямого відкриття або оновлення URL відображається повна сторінка.
Для закриття модального маршруту зазвичай використовується router.back().