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:
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.
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.tsxWyobraź 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}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}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 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.
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/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/1231// 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 >
38 ✕
39 </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}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 logowania1// 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}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.
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: /settings1// 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}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 # /profileRoute 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.tsx1// 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}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.tsx1// 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}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/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.tsx1// ✅ 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)/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:
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.