Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Zaawansowany routing w Next.js 15 - Parallel Routes, Intercepting Routes i Route Groups

W Metropolii Quantum, zaawansowany system transportowy pozwala na jednoczesne podróżowanie wieloma ścieżkami równolegle, przechwytywanie i przekierowywanie ruchu w locie oraz logiczne grupowanie tras bez wpływu na ich adresy. Podobnie, Next.js 15 oferuje zaawansowane techniki routingu, które pozwalają na tworzenie złożonych interfejsów użytkownika z elegancją i efektywnością.

W tym rozdziale poznasz trzy potężne wzorce routingu w App Router:

  1. Parallel Routes - renderowanie wielu tras jednocześnie
  2. Intercepting Routes - przechwytywanie nawigacji i pokazywanie alternatywnych widoków
  3. Route Groups - organizowanie tras bez wpływu na URL

Parallel Routes - Równoległe renderowanie tras

Czym są Parallel Routes?

Parallel Routes pozwalają na jednoczesne renderowanie wielu stron (lub części stron) w tym samym layoucie. To jak holograficzne wyświetlacze w Metropolii Quantum, które mogą pokazywać wiele niezależnych strumieni informacji jednocześnie.

Konwencja nazewnictwa - @folder

Parallel Routes używają specjalnej konwencji nazewnictwa z prefiksem

@
:

1app/
2├── layout.tsx
3├── page.tsx
4├── @analytics/       # Slot "analytics"
5│   └── page.tsx
6├── @team/           # Slot "team"
7│   └── page.tsx
8└── @activity/       # Slot "activity"
9    └── page.tsx

Podstawowy przykład - Dashboard z wieloma panelami

Wyobraź sobie panel kontrolny Metropolii Quantum, który jednocześnie wyświetla analitykę, aktywność zespołu i ostatnie wydarzenia:

1// app/dashboard/layout.tsx
2export default function DashboardLayout({
3  children,
4  analytics,
5  team,
6  activity,
7}: {
8  children: React.ReactNode;
9  analytics: React.ReactNode;
10  team: React.ReactNode;
11  activity: React.ReactNode;
12}) {
13  return (
14    <div className="quantum-dashboard">
15      <div className="main-content">
16        {children}
17      </div>
18
19      <div className="grid grid-cols-3 gap-6 mt-6">
20        <div className="panel">
21          <h2>Analityka kwantowa</h2>
22          {analytics}
23        </div>
24
25        <div className="panel">
26          <h2>Zespół</h2>
27          {team}
28        </div>
29
30        <div className="panel">
31          <h2>Ostatnia aktywność</h2>
32          {activity}
33        </div>
34      </div>
35    </div>
36  );
37}
1// app/dashboard/@analytics/page.tsx
2async function getAnalytics() {
3  const res = await fetch('https://api.quantum.com/analytics', {
4    next: { revalidate: 60 }
5  });
6  return res.json();
7}
8
9export default async function Analytics() {
10  const data = await getAnalytics();
11
12  return (
13    <div className="analytics-panel">
14      <div className="stat">
15        <span className="label">Energia kwantowa</span>
16        <span className="value">{data.energy}%</span>
17      </div>
18      <div className="stat">
19        <span className="label">Wydajność systemu</span>
20        <span className="value">{data.efficiency}%</span>
21      </div>
22    </div>
23  );
24}
1// app/dashboard/@team/page.tsx
2async function getTeam() {
3  const res = await fetch('https://api.quantum.com/team', {
4    next: { revalidate: 300 }
5  });
6  return res.json();
7}
8
9export default async function Team() {
10  const members = await getTeam();
11
12  return (
13    <ul className="team-list">
14      {members.map(member => (
15        <li key={member.id} className="team-member">
16          <img src={member.avatar} alt={member.name} />
17          <span>{member.name}</span>
18          <span className="status">{member.status}</span>
19        </li>
20      ))}
21    </ul>
22  );
23}
1// app/dashboard/@activity/page.tsx
2async function getActivity() {
3  const res = await fetch('https://api.quantum.com/activity', {
4    cache: 'no-store' // Zawsze świeże dane
5  });
6  return res.json();
7}
8
9export default async function Activity() {
10  const activities = await getActivity();
11
12  return (
13    <div className="activity-feed">
14      {activities.map(activity => (
15        <div key={activity.id} className="activity-item">
16          <span className="timestamp">{activity.time}</span>
17          <p>{activity.description}</p>
18        </div>
19      ))}
20    </div>
21  );
22}

Zalety Parallel Routes

  1. Niezależne ładowanie - każdy slot może być ładowany niezależnie z własnym stanem ładowania
  2. Niezależne błędy - błąd w jednym slocie nie wpływa na inne
  3. Selektywna nawigacja - możesz nawigować w obrębie jednego slotu bez wpływu na inne
  4. Warunkowe renderowanie - możesz warunkowo pokazywać/ukrywać sloty

Default.tsx - Obsługa brakujących slotów

Gdy użytkownik nawiguje w sposób, który nie pasuje do struktury slotów, Next.js używa pliku

default.tsx
:

1// app/dashboard/@analytics/default.tsx
2export default function DefaultAnalytics() {
3  return <div>Brak danych analitycznych</div>;
4}

Warunkowe renderowanie slotów

1// app/dashboard/layout.tsx
2export default async function DashboardLayout({
3  children,
4  analytics,
5  team,
6  activity,
7  params,
8}: {
9  children: React.ReactNode;
10  analytics: React.ReactNode;
11  team: React.ReactNode;
12  activity: React.ReactNode;
13  params: Promise<{ userRole: string }>;
14}) {
15  const isAdmin = (await params).userRole === 'admin';
16
17  return (
18    <div className="quantum-dashboard">
19      <div className="main-content">{children}</div>
20
21      <div className="panels">
22        {analytics}
23        {team}
24        {isAdmin && activity} {/* Tylko dla adminów */}
25      </div>
26    </div>
27  );
28}

Intercepting Routes - Przechwytywanie nawigacji

Czym są Intercepting Routes?

Intercepting Routes pozwalają na przechwytywanie nawigacji i wyświetlanie alternatywnego widoku, podczas gdy URL się zmienia. To jak kwantowe tunele w Metropolii Quantum - pozwalają na krótkie ścieżki bez zmiany docelowej lokalizacji.

Konwencja nazewnictwa

Intercepting Routes używają specjalnych prefiksów:

  • (.)
    - przechwytuje segmenty na tym samym poziomie
  • (..)
    - przechwytuje segmenty jeden poziom wyżej
  • (..)(..)
    - przechwytuje segmenty dwa poziomy wyżej
  • (...)
    - przechwytuje segmenty z roota
    app/

Przykład 1: Modal z obrazkiem

Klasyczny przypadek użycia - galeria zdjęć, gdzie kliknięcie otwiera modal, ale URL się zmienia:

1app/
2├── layout.tsx
3├── page.tsx              # Galeria /
4├── photos/
5│   └── [id]/
6│       └── page.tsx      # Pełna strona zdjęcia /photos/123
7└── @modal/
8    └── (..)photos/
9        └── [id]/
10            └── page.tsx  # Modal przechwytujący /photos/123
1// app/page.tsx - Galeria
2import Link from 'next/link';
3import Image from 'next/image';
4
5const photos = [
6  { id: '1', url: '/photos/quantum-city-1.jpg', title: 'Dystrykt Kwantowy' },
7  { id: '2', url: '/photos/quantum-city-2.jpg', title: 'Centralna Plaza' },
8  { id: '3', url: '/photos/quantum-city-3.jpg', title: 'Skyline nocą' },
9];
10
11export default function Gallery() {
12  return (
13    <div className="gallery-grid">
14      {photos.map(photo => (
15        <Link key={photo.id} href={`/photos/${photo.id}`}>
16          <div className="photo-card">
17            <Image
18              src={photo.url}
19              alt={photo.title}
20              width={300}
21              height={200}
22            />
23            <p>{photo.title}</p>
24          </div>
25        </Link>
26      ))}
27    </div>
28  );
29}
1// app/photos/[id]/page.tsx - Pełna strona zdjęcia
2import Image from 'next/image';
3
4async function getPhoto(id: string) {
5  // Pobierz dane zdjęcia
6  return {
7    id,
8    url: `/photos/quantum-city-${id}.jpg`,
9    title: 'Quantum City',
10    description: 'Widok na Metropolię Quantum',
11  };
12}
13
14export default async function PhotoPage({
15  params,
16}: {
17  params: Promise<{ id: string }>;
18}) {
19  const photo = await getPhoto((await params).id);
20
21  return (
22    <div className="photo-full-page">
23      <h1>{photo.title}</h1>
24      <Image
25        src={photo.url}
26        alt={photo.title}
27        width={1200}
28        height={800}
29      />
30      <p>{photo.description}</p>
31    </div>
32  );
33}
1// app/@modal/(.)photos/[id]/page.tsx - Modal
2'use client';
3
4import { useRouter } from 'next/navigation';
5import Image from 'next/image';
6import { useEffect, useState } from 'react';
7
8export default function PhotoModal({
9  params,
10}: {
11  params: { id: string };
12}) {
13  const router = useRouter();
14  const [photo, setPhoto] = useState(null);
15
16  useEffect(() => {
17    // Pobierz dane zdjęcia
18    fetch(`/api/photos/${params.id}`)
19      .then(res => res.json())
20      .then(setPhoto);
21  }, [params.id]);
22
23  if (!photo) return null;
24
25  return (
26    <div
27      className="modal-overlay"
28      onClick={() => router.back()}
29    >
30      <div
31        className="modal-content"
32        onClick={(e) => e.stopPropagation()}
33      >
34        <button
35          className="modal-close"
36          onClick={() => router.back()}
37        >
3839        </button>
40
41        <Image
42          src={photo.url}
43          alt={photo.title}
44          width={800}
45          height={600}
46        />
47
48        <h2>{photo.title}</h2>
49        <p>{photo.description}</p>
50      </div>
51    </div>
52  );
53}
1// app/layout.tsx - Layout z modalem
2export default function RootLayout({
3  children,
4  modal,
5}: {
6  children: React.ReactNode;
7  modal: React.ReactNode;
8}) {
9  return (
10    <html lang="pl">
11      <body>
12        {children}
13        {modal}
14      </body>
15    </html>
16  );
17}

Przykład 2: Login modal z intercepting route

1app/
2├── layout.tsx
3├── page.tsx
4├── login/
5│   └── page.tsx          # Pełna strona logowania /login
6└── @auth/
7    └── (.)login/
8        └── page.tsx      # Modal logowania
1// app/@auth/(.)login/page.tsx
2'use client';
3
4import { useRouter } from 'next/navigation';
5import { useState } from 'react';
6
7export default function LoginModal() {
8  const router = useRouter();
9  const [email, setEmail] = useState('');
10  const [password, setPassword] = useState('');
11
12  const handleSubmit = async (e: React.FormEvent) => {
13    e.preventDefault();
14    // Logika logowania
15    await loginUser(email, password);
16    router.back();
17  };
18
19  return (
20    <div className="modal-overlay" onClick={() => router.back()}>
21      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
22        <h2>Zaloguj się do Metropolii Quantum</h2>
23
24        <form onSubmit={handleSubmit}>
25          <input
26            type="email"
27            value={email}
28            onChange={(e) => setEmail(e.target.value)}
29            placeholder="Email"
30            required
31          />
32
33          <input
34            type="password"
35            value={password}
36            onChange={(e) => setPassword(e.target.value)}
37            placeholder="Hasło"
38            required
39          />
40
41          <button type="submit">Zaloguj</button>
42        </form>
43      </div>
44    </div>
45  );
46}

Zalety Intercepting Routes

  1. Lepsze UX - szybkie modali bez pełnego przeładowania strony
  2. Shareable URLs - URL się zmienia, więc można udostępnić link
  3. Graceful fallback - bezpośrednie wejście na URL pokazuje pełną stronę
  4. Zachowanie kontekstu - tło pozostaje widoczne pod modalem

Route Groups - Organizacja tras

Czym są Route Groups?

Route Groups pozwalają na logiczne grupowanie tras bez wpływu na strukturę URL. To jak dzielnice w Metropolii Quantum - organizują miasto, ale nie wpływają na adresy budynków.

Konwencja nazewnictwa - (folder)

Route Groups używają nawiasów okrągłych w nazwach folderów:

1app/
2├── (marketing)/        # Grupa - nie wpływa na URL
3│   ├── layout.tsx      # Layout dla marketingu
4│   ├── about/
5│   │   └── page.tsx    # URL: /about
6│   └── contact/
7│       └── page.tsx    # URL: /contact
8├── (shop)/            # Grupa - nie wpływa na URL
9│   ├── layout.tsx      # Layout dla sklepu
10│   ├── products/
11│   │   └── page.tsx    # URL: /products
12│   └── cart/
13│       └── page.tsx    # URL: /cart
14└── (dashboard)/       # Grupa - nie wpływa na URL
15    ├── layout.tsx      # Layout dla dashboardu
16    ├── analytics/
17    │   └── page.tsx    # URL: /analytics
18    └── settings/
19        └── page.tsx    # URL: /settings

Przykład 1: Różne layouty dla różnych sekcji

1// app/(marketing)/layout.tsx
2import MarketingHeader from '@/components/MarketingHeader';
3import MarketingFooter from '@/components/MarketingFooter';
4
5export default function MarketingLayout({
6  children,
7}: {
8  children: React.ReactNode;
9}) {
10  return (
11    <div className="marketing-layout">
12      <MarketingHeader />
13      <main className="marketing-content">
14        {children}
15      </main>
16      <MarketingFooter />
17    </div>
18  );
19}
1// app/(dashboard)/layout.tsx
2import DashboardSidebar from '@/components/DashboardSidebar';
3import DashboardHeader from '@/components/DashboardHeader';
4
5export default function DashboardLayout({
6  children,
7}: {
8  children: React.ReactNode;
9}) {
10  return (
11    <div className="dashboard-layout">
12      <DashboardHeader />
13      <div className="dashboard-container">
14        <DashboardSidebar />
15        <main className="dashboard-content">
16          {children}
17        </main>
18      </div>
19    </div>
20  );
21}

Przykład 2: Organizacja według funkcjonalności

1app/
2├── (auth)/
3│   ├── login/
4│   │   └── page.tsx     # /login
5│   ├── register/
6│   │   └── page.tsx     # /register
7│   └── forgot-password/
8│       └── page.tsx     # /forgot-password
9├── (public)/
10│   ├── about/
11│   │   └── page.tsx     # /about
12│   └── blog/
13│       └── [slug]/
14│           └── page.tsx # /blog/:slug
15└── (protected)/
16    ├── middleware.ts    # Middleware tylko dla tej grupy
17    ├── dashboard/
18    │   └── page.tsx     # /dashboard
19    └── profile/
20        └── page.tsx     # /profile

Przykład 3: Multiple root layouts

Route Groups pozwalają na tworzenie wielu root layoutów w tej samej aplikacji:

1app/
2├── (shop)/
3│   ├── layout.tsx       # Root layout dla sklepu
4│   └── products/
5│       └── page.tsx
6└── (blog)/
7    ├── layout.tsx       # Root layout dla bloga
8    └── posts/
9        └── page.tsx
1// app/(shop)/layout.tsx
2export default function ShopRootLayout({
3  children,
4}: {
5  children: React.ReactNode;
6}) {
7  return (
8    <html lang="pl">
9      <body className="shop-theme">
10        {children}
11      </body>
12    </html>
13  );
14}
1// app/(blog)/layout.tsx
2export default function BlogRootLayout({
3  children,
4}: {
5  children: React.ReactNode;
6}) {
7  return (
8    <html lang="pl">
9      <body className="blog-theme">
10        {children}
11      </body>
12    </html>
13  );
14}

Zalety Route Groups

  1. Organizacja kodu - logiczne grupowanie bez wpływu na URL
  2. Różne layouty - możliwość stosowania różnych layoutów dla różnych sekcji
  3. Lepsza struktura projektu - łatwiejsze zarządzanie dużymi aplikacjami
  4. Separacja odpowiedzialności - różne zespoły mogą pracować nad różnymi grupami

Łączenie wszystkich trzech technik

Prawdziwa moc App Router objawia się, gdy łączysz te trzy techniki razem:

1app/
2├── (marketing)/
3│   ├── layout.tsx
4│   ├── page.tsx
5│   └── @modal/
6│       └── (.)contact/
7│           └── page.tsx
8├── (dashboard)/
9│   ├── layout.tsx
10│   ├── @analytics/
11│   │   └── page.tsx
12│   ├── @team/
13│   │   └── page.tsx
14│   └── @activity/
15│       └── page.tsx
16└── (shop)/
17    ├── layout.tsx
18    ├── products/
19    │   └── [id]/
20    │       └── page.tsx
21    └── @cart/
22        └── (.)products/
23            └── [id]/
24                └── page.tsx

Przykład: E-commerce z zaawansowanym routingiem

1// app/(shop)/layout.tsx
2export default function ShopLayout({
3  children,
4  cart,
5}: {
6  children: React.ReactNode;
7  cart: React.ReactNode;
8}) {
9  return (
10    <div className="shop-layout">
11      <ShopHeader />
12      <main>{children}</main>
13      {cart}
14      <ShopFooter />
15    </div>
16  );
17}
1// app/(shop)/products/[id]/page.tsx
2async function getProduct(id: string) {
3  const res = await fetch(`https://api.quantum.com/products/${id}`);
4  return res.json();
5}
6
7export default async function ProductPage({
8  params,
9}: {
10  params: Promise<{ id: string }>;
11}) {
12  const product = await getProduct((await params).id);
13
14  return (
15    <div className="product-page">
16      <h1>{product.name}</h1>
17      <p>{product.description}</p>
18      <button>Dodaj do koszyka</button>
19    </div>
20  );
21}
1// app/(shop)/@cart/(.)products/[id]/page.tsx
2'use client';
3
4import { useRouter } from 'next/navigation';
5
6export default function QuickViewModal({
7  params,
8}: {
9  params: { id: string };
10}) {
11  const router = useRouter();
12
13  return (
14    <div className="modal-overlay" onClick={() => router.back()}>
15      <div className="quick-view" onClick={(e) => e.stopPropagation()}>
16        <ProductQuickView productId={params.id} />
17        <button onClick={() => router.back()}>Zamknij</button>
18      </div>
19    </div>
20  );
21}

Najlepsze praktyki

1. Używaj Parallel Routes do niezależnych sekcji

1// ✅ Dobre - niezależne panele
2app/dashboard/
3├── @analytics/
4├── @team/
5└── @notifications/
6
7// ❌ Złe - zależne komponenty
8app/dashboard/
9├── @header/      // To powinien być komponent, nie slot
10├── @footer/      // To powinien być komponent, nie slot
11└── @content/

2. Używaj Intercepting Routes do modali i overlays

1// ✅ Dobre
2app/
3├── photos/[id]/page.tsx
4└── @modal/(.)photos/[id]/page.tsx
5
6// ❌ Złe - modala jako osobna trasa
7app/
8├── photos/[id]/page.tsx
9└── modal/photos/[id]/page.tsx

3. Używaj Route Groups do organizacji

1// ✅ Dobre - logiczne grupowanie
2app/
3├── (marketing)/
4├── (dashboard)/
5└── (auth)/
6
7// ❌ Złe - grupowanie według typu pliku
8app/
9├── (pages)/
10├── (components)/
11└── (api)/

Podsumowanie

Zaawansowane techniki routingu w Next.js 15 to jak zaawansowana sieć transportowa Metropolii Quantum - pozwalają na tworzenie złożonych, wydajnych i eleganckich interfejsów użytkownika:

  1. Parallel Routes - renderuj wiele tras jednocześnie dla niezależnych sekcji
  2. Intercepting Routes - przechwytuj nawigację dla lepszego UX z modali i overlays
  3. Route Groups - organizuj kod bez wpływu na URL

Te techniki dają ci niesamowitą kontrolę nad strukturą aplikacji i doświadczeniem użytkownika. Podobnie jak inżynierowie Metropolii Quantum projektują wielopoziomowe systemy transportowe dla maksymalnej efektywności, tak i ty możesz projektować zaawansowane struktury routingu dla optymalnego UX.

Kluczem do sukcesu jest zrozumienie, kiedy używać każdej techniki i jak je łączyć, aby stworzyć spójną, wydajną aplikację Next.js 15.

Przejdź do CodeWorlds