Урок 07

Параллельные и перехватывающие маршруты

Продвинутые возможности App Router для сложных UI.

Параллельные маршруты

Позволяют рендерить несколько страниц в одном layout одновременно.

Синтаксис

Папки с @ — слоты:

app/
├── layout.tsx
├── page.tsx
├── @team/
│   └── page.tsx
└── @analytics/
    └── page.tsx

Layout

// app/layout.tsx
export default function Layout({
    children,
    team,
    analytics,
}: {
    children: React.ReactNode;
    team: React.ReactNode;
    analytics: React.ReactNode;
}) {
    return (
        <>
            {children}
            <div className="grid">
                {team}
                {analytics}
            </div>
        </>
    );
}

Каждый слот рендерится параллельно и независимо.

default.tsx

Если слот не совпал — используется default.tsx:

// app/@team/default.tsx
export default function Default() {
    return null;
}

Иначе — 404.

Перехватывающие маршруты

Позволяют показать маршрут в контексте другого. Классический случай — модалка поверх списка.

Синтаксис

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

При клике на фото из /feed открывается модалка. При прямом заходе /photo/1 — отдельная страница.

Пример

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

export default async function PhotoModal({ params }) {
    const { id } = await params;
    const photo = await getPhoto(id);

    return (
        <div className="modal">
            <Image src={photo.url} alt={photo.alt} />
        </div>
    );
}

app/photo/[id]/page.tsx:

export default async function PhotoPage({ params }) {
    const { id } = await params;
    const photo = await getPhoto(id);

    return (
        <div className="photo-page">
            <Image src={photo.url} alt={photo.alt} />
            <Link href="/feed">К ленте</Link>
        </div>
    );
}

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

'use client';

import { useRouter } from 'next/navigation';

export function Modal({ children }) {
    const router = useRouter();

    return (
        <div className="backdrop" onClick={() => router.back()}>
            <div className="content" onClick={(e) => e.stopPropagation()}>
                {children}
            </div>
        </div>
    );
}

router.back() возвращает на предыдущий URL.

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

Параллельные:

  • Дашборд с несколькими панелями
  • Модалки поверх страниц
  • Разные layouts для разных секций

Перехватывающие:

  • Модалки, сохраняющие URL
  • Галереи, где фото открывается поверх ленты
  • Логин-модалка вместо отдельной страницы

Сложности

  • Требуют внимательного планирования структуры
  • default.tsx обязателен для слотов
  • URL и UI могут расходиться
  • Не всегда очевидно, какой маршрут перехватится

Итоги

  • @slot — параллельные маршруты
  • (.), (..), (...) — перехват
  • Модалки без потери URL
  • default.tsx для слотов
  • Используются для сложных UI