Пошук уроків, статей та іншого контенту
Перехоплюйте навігацію та відображайте маршрут у модальному вікні за допомогою Intercepting Routes.
Intercepting Routes в Next.js App Router дають змогу перехопити навігацію до маршруту й показати його в іншому представленні — найчастіше в модальному вікні.
Наприклад:
користувач переходить зі списку фотографій на /photos/1;
URL змінюється на /photos/1;
замість повної сторінки відкривається модальне вікно;
після оновлення сторінки або прямого переходу за URL відображається повна сторінка фотографії.
Це корисно для:
галерей;
сторінок товарів;
перегляду профілів;
карток постів;
діалогів із детальною інформацією.
Intercepting Routes працюють разом із Parallel Routes, оскільки модальне вікно зазвичай рендериться в окремому slot.
Next.js визначає Intercepting Route за спеціальними назвами сегментів:
(.) — перехопити маршрут на тому самому рівні;
(..) — перехопити маршрут на один рівень вище;
(..)(..) — перехопити маршрут на два рівні вище;
(...)
appНаприклад:
app/
├── photos/
│ └── [id]/
│ └── page.tsx
└── @modal/
└── (.)photos/
└── [id]/
└── page.tsxФайл:
app/@modal/(.)photos/[id]/page.tsxперехоплює маршрут:
app/photos/[id]/page.tsxСегмент @modal — це Parallel Route slot, а (.)photos — Intercepting Route.
Intercepting Routes мають різну поведінку залежно від типу навігації.
М'яка навігація відбувається, коли користувач натискає на Link у вже завантаженому застосунку.
У цьому випадку Next.js може перехопити маршрут і показати модальне вікно:
список фотографій → /photos/1 → модальне вікноЖорстка навігація відбувається, коли користувач:
оновлює сторінку;
відкриває URL у новій вкладці;
вводить URL вручну;
переходить на сторінку після повного перезавантаження.
У цьому випадку Next.js показує звичайну сторінку:
/photos/1 → повна сторінка фотографіїЦе дозволяє мати одночасно:
зручний перегляд у модальному вікні під час навігації;
повноцінну доступну сторінку для прямого URL.
Створимо галерею фотографій із таким маршрутом:
app/
├── @modal/
│ ├── (.)photos/
│ │ └── [id]/
│ │ └── page.tsx
│ └── default.tsx
├── components/
│ ├── Modal.tsx
│ └── PhotoView.tsx
├── photos/
│ └── [id]/
│ └── page.tsx
├── globals.css
├── layout.tsx
└── page.tsx
lib/
└── photos.tsФайл lib/photos.ts:
export type Photo = {
id: string
title: string
description: string
color: string
}
const photos: Photo[] = [
{
id: '1',
title: 'Гори',
description: 'Пейзаж із горами під час сходу сонця.',
color: '#4f46e5',
},
{
id: '2',
title: 'Ліс',
description: 'Зелений ліс після дощу.',
color: '#047857',
},
{
id: '3',
title: 'Море',
description: 'Спокійне море на заході сонця.',
color: '#0369a1',
},
]
export function getPhotos() {
return photos
}
export function getPhoto(id: string) {
return photos.find((photo) => photo.id === id)
}Повна сторінка та модальне вікно показуватимуть однакові дані. Щоб не дублювати розмітку, винесемо її в компонент.
Файл app/components/PhotoView.tsx:
import type { Photo } from '@/lib/photos'
type PhotoViewProps = {
photo: Photo
}
export default function PhotoView({ photo }: PhotoViewProps) {
return (
<article>
<div
style={{
height: 320,
backgroundColor: photo.color,
display: 'grid',
placeItems: 'center',
color: 'white',
fontSize: 48,
fontWeight: 700,
}}
>
{photo.title}
</div>
<h1>{photo.title}</h1>
<p>{photo.description}</p>
</article>
)
}Це звичайний маршрут, який буде відображатися під час прямого переходу або оновлення сторінки.
Файл app/photos/[id]/page.tsx:
import { notFound } from 'next/navigation'
import PhotoView from '@/app/components/PhotoView'
import { getPhoto } from '@/lib/photos'
type PhotoPageProps = {
params: Promise<{
id: string
}>
}
export default async function PhotoPage({ params }: PhotoPageProps) {
const { id } = await params
const photo = getPhoto(id)
if (!photo) {
notFound()
}
return (
<main>
<PhotoView photo={photo} />
</main>
)
}notFound() показує сторінку 404, якщо фотографії з таким ідентифікатором не існує.
Створимо @modal — Parallel Route slot для модального представлення.
Файл app/@modal/default.tsx:
export default function DefaultModal() {
return null
}default.tsx потрібен для випадків, коли модальний маршрут не активний. Без нього Next.js може не знати, що рендерити в slot під час початкового завантаження.
Файл app/layout.tsx:
import type { ReactNode } from 'react'
import './globals.css'
type RootLayoutProps = {
children: ReactNode
modal: ReactNode
}
export default function RootLayout({
children,
modal,
}: RootLayoutProps) {
return (
<html lang="uk">
<body>
{children}
{modal}
</body>
</html>
)
}Назва пропса modal відповідає назві slot @modal.
Папка з префіксом @ не додається до URL. Тому @modal є лише способом організувати паралельне представлення маршруту.
Файл app/page.tsx:
import Link from 'next/link'
import { getPhotos } from '@/lib/photos'
export default function HomePage() {
const photos = getPhotos()
return (
<main>
<h1>Галерея</h1>
<ul className="photo-list">
{photos.map((photo) => (
<li key={photo.id}>
<Link href={`/photos/${photo.id}`}>
{photo.title}
</Link>
</li>
))}
</ul>
</main>
)
}Під час натискання на Link Next.js виконує клієнтську навігацію. Саме в цей момент може спрацювати Intercepting Route.
Модальному вікну потрібно керувати історією браузера. Для цього використаємо useRouter().back().
Файл app/components/Modal.tsx:
'use client'
import type { ReactNode } from 'react'
import { useRouter } from 'next/navigation'
type ModalProps = {
children: ReactNode
}
export default function Modal({ children }: ModalProps) {
const router = useRouter()
function closeModal() {
router.back()
}
return (
<div
className="modal-backdrop"
role="presentation"
onClick={closeModal}
>
<section
className="modal"
role="dialog"
aria-modal="true"
onClick={(event) => event.stopPropagation()}
>
<button
type="button"
className="modal-close"
onClick={closeModal}
aria-label="Закрити модальне вікно"
>
×
</button>
{children}
</section>
</div>
)
}Обробник на backdrop закриває модальне вікно під час натискання поза його межами. stopPropagation() не дозволяє закрити його під час натискання всередині.
Тепер створимо маршрут:
app/@modal/(.)photos/[id]/page.tsxФайл app/@modal/(.)photos/[id]/page.tsx:
import { notFound } from 'next/navigation'
import Modal from '@/app/components/Modal'
import PhotoView from '@/app/components/PhotoView'
import { getPhoto } from '@/lib/photos'
type ModalPhotoPageProps = {
params: Promise<{
id: string
}>
}
export default async function ModalPhotoPage({
params,
}: ModalPhotoPageProps) {
const { id } = await params
const photo = getPhoto(id)
if (!photo) {
notFound()
}
return (
<Modal>
<PhotoView photo={photo} />
</Modal>
)
}Частина (.)photos означає, що цей маршрут перехоплює сегмент photos на тому самому рівні.
Тепер послідовність роботи така:
Користувач відкриває головну сторінку.
Натискає посилання /photos/1.
Next.js виконує м'яку навігацію.
Intercepting Route перехоплює маршрут.
Канонічний контент відображається всередині Modal.
URL стає /photos/1.
Натискання кнопки закриття викликає router.back().
Якщо користувач одразу відкриє /photos/1 або оновить сторінку, відобразиться app/photos/[id]/page.tsx, а не модальне вікно.
Файл app/globals.css:
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
min-height: 100%;
font-family: sans-serif;
}
body {
padding: 32px;
background: #f8fafc;
color: #0f172a;
}
main {
max-width: 720px;
margin: 0 auto;
}
.photo-list {
display: grid;
gap: 12px;
padding: 0;
list-style: none;
}
.photo-list a {
display: block;
padding: 16px;
border-radius: 8px;
background: white;
color: #1d4ed8;
text-decoration: none;
}
.photo-list a:hover {
background: #dbeafe;
}
.modal-backdrop {
position: fixed;
inset: 0;
z-index: 10;
display: grid;
place-items: center;
padding: 24px;
background: rgb(15 23 42 / 60%);
}
.modal {
position: relative;
width: min(100%, 640px);
max-height: calc(100vh - 48px);
overflow: auto;
padding: 24px;
border-radius: 12px;
background: white;
box-shadow: 0 20px 50px rgb(15 23 42 / 30%);
}
.modal-close {
position: absolute;
top: 8px;
right: 12px;
z-index: 1;
border: 0;
background: transparent;
color: #334155;
font-size: 32px;
line-height: 1;
cursor: pointer;
}router.back()Модальне вікно є результатом переходу на новий URL. Коли користувач його закриває, логічно повернутися до попереднього запису в історії браузера:
router.back()Це також означає, що кнопка «Назад» у браузері закриє модальне вікно так само, як і кнопка всередині нього.
Після закриття:
URL повертається до попереднього;
модальний slot знову рендерить default.tsx;
список фотографій залишається на поточному стані сторінки.
Важливо мати два маршрути:
app/photos/[id]/page.tsx
app/@modal/(.)photos/[id]/page.tsxПерший маршрут — це канонічна сторінка.
Другий маршрут — альтернативне представлення для м'якої навігації.
Так URL /photos/1 залишається повноцінним ресурсом, який можна:
зберегти в закладки;
відкрити в новій вкладці;
передати іншому користувачу;
завантажити після оновлення сторінки.
default.tsxЯкщо для Parallel Route slot не створити default.tsx, під час початкового завантаження можуть виникнути помилки рендерингу.
Створіть файл:
app/@modal/default.tsxі поверніть із нього null або резервний інтерфейс.
Назва:
(.)photosперехоплює сегмент photos на тому самому рівні.
Якщо використати неправильний префікс, маршрут не буде перехоплений або перехопить інший сегмент.
Link використовується повне перезавантаженняIntercepting Routes розраховані на м'яку навігацію. Посилання має бути таким:
<Link href="/photos/1">Гори</Link>А не таким:
<a href="/photos/1">Гори</a>Звичайний <a> може спричинити повне завантаження сторінки, тому модальне представлення не буде використане.
Не варто закривати модальне вікно лише зміною локального стану:
setIsOpen(false)У такому разі URL залишиться /photos/1, хоча модальне вікно зникне.
Для синхронізації з історією браузера використовуйте:
router.back()Intercepting Route не повинен бути єдиним представленням ресурсу. Для прямого відкриття URL потрібен звичайний маршрут:
app/photos/[id]/page.tsxДля папки:
app/@modal/у layout потрібно прийняти пропс:
function Layout({ children, modal }) {
// ...
}Назва modal має відповідати назві після @.
Intercepting Routes дають змогу показувати маршрут у модальному вікні під час м'якої навігації.
Спеціальні префікси (.), (..), (..)(..), (...) визначають рівень перехоплення.
Для модальних вікон Intercepting Routes зазвичай використовують разом із Parallel Route slot.
app/@modal/default.tsx визначає стан slot, коли модальне вікно неактивне.
Під час прямого переходу або оновлення сторінки відображається канонічний маршрут.
Для закриття модального вікна з поверненням у браузерну історію зручно використовувати router.back().
Канонічна сторінка та модальне представлення мають використовувати один і той самий URL, але різний спосіб відображення.