Урок 11

Продвинутый роутинг: parallel и intercepting

Next.js App Router поддерживает сложные схемы роутинга: параллельные маршруты и перехватывающие.

Parallel Routes

Параллельные маршруты позволяют рендерить несколько страниц в одном layout одновременно. Обозначаются через @:

app/
├── @modal/
│   └── login/
│       └── page.tsx
├── @sidebar/
│   └── page.tsx
├── layout.tsx
└── page.tsx

Layout получает их как пропсы:

export default function Layout({
    children,
    modal,
    sidebar,
}: {
    children: React.ReactNode;
    modal: React.ReactNode;
    sidebar: React.ReactNode;
}) {
    return (
        <>
            <aside>{sidebar}</aside>
            <main>{children}</main>
            {modal}
        </>
    );
}

Слоты со своими loading и error

Каждый слот имеет свой loading.tsx и error.tsx:

@modal/
├── login/
│   └── page.tsx
├── loading.tsx
└── error.tsx

Intercepting Routes

Перехватывающие маршруты показывают одну страницу в контексте другой. Обозначаются через (.), (..), (...):

app/
├── feed/
│   └── page.tsx
├── photo/
│   └── [id]/
│       └── page.tsx
└── (.)photo/
    └── [id]/
        └── page.tsx
  • (.) — тот же уровень
  • (..) — уровень выше
  • (..)(..) — два уровня выше
  • (...) — из корня app

Практика: модалка из URL

Классический сценарий: клик по фото в ленте открывает модалку, но URL меняется на /photo/123.

app/feed/page.tsx:

import Link from 'next/link';

export default function Feed() {
    return (
        <div>
            {photos.map((photo) => (
                <Link key={photo.id} href={`/photo/${photo.id}`}>
                    <img src={photo.thumbnail} alt="" />
                </Link>
            ))}
        </div>
    );
}

app/@modal/(.)photo/[id]/page.tsx:

export default async function PhotoModal({ params }: { params: Promise<{ id: string }> }) {
    const { id } = await params;
    return (
        <div className="modal">
            <img src={`/photos/${id}.jpg`} alt="" />
        </div>
    );
}

app/photo/[id]/page.tsx — полная страница, если открыть URL напрямую:

export default async function PhotoPage({ params }: { params: Promise<{ id: string }> }) {
    const { id } = await params;
    return (
        <div>
            <img src={`/photos/${id}.jpg`} alt="" />
            <p>Полная страница фото</p>
        </div>
    );
}

Закрытие модалки

Через router.back() или через отдельный маршрут, который делает редирект:

'use client';

import { useRouter } from 'next/navigation';

export default function Modal() {
    const router = useRouter();

    return (
        <div className="modal" onClick={() => router.back()}>
            {/* содержимое */}
        </div>
    );
}

Когда использовать

Parallel routes — когда нужно несколько независимых областей на одной странице: дашборд, sidebar, modal, чат.

Intercepting routes — когда нужны модалки с сохранением URL: галереи, детали из списка, авторизация.

Задание

  1. Создай слот @modal в layout
  2. Сделай перехватывающий маршрут для /photo/[id]
  3. Проверь: клик открывает модалку, прямой заход — полную страницу
  4. Добавь router.back() для закрытия

Что дальше

Следующий курс: «Данные и кеширование» — загрузка данных, кеш, ревалидация.