Cours Next.js · Module 2: Routing et layouts

Routage avancé dans Next.js 16 - Parallel Routes, Intercepting Routes et Route Groups

14 min de lecture
Dans cette leçon6

Dans la métropole quantique de Metropolis Quantum, le système de transport avancé permet de voyager simultanément sur plusieurs chemins parallèles, d'intercepter et de rediriger le trafic en plein vol, et de regrouper logiquement les itinéraires sans affecter leurs adresses. De la même manière, Next.js 16 propose des techniques de routage avancées qui permettent de créer des interfaces utilisateur complexes avec élégance et efficacité.

Dans ce chapitre, tu vas apprendre trois patterns de routage puissants dans l'App Router :

  1. Parallel Routes - le rendu simultané de plusieurs routes
  2. Intercepting Routes - l'interception de la navigation et l'affichage de vues alternatives
  3. Route Groups - l'organisation des routes sans affecter l'URL

Parallel Routes - Rendu simultané de routes

Que sont les Parallel Routes ?

Les Parallel Routes permettent le rendu simultané de plusieurs pages (ou parties de pages) dans une même mise en page. C'est comme les affichages holographiques de Metropolis Quantum, qui peuvent montrer simultanément plusieurs flux d'informations indépendants.

Convention de nommage - @folder

Les Parallel Routes utilisent une convention de nommage spéciale avec le préfixe @ :

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

Exemple de base - Tableau de bord avec plusieurs panneaux

Imagine un panneau de contrôle de Metropolis Quantum qui affiche simultanément les analytics, l'activité de l'équipe et les événements récents :

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>Analytics quantiques</h2>
22          {analytics}
23        </div>
24
25        <div className="panel">
26          <h2>Équipe</h2>
27          {team}
28        </div>
29
30        <div className="panel">
31          <h2>Activité récente</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">Énergie quantique</span>
16        <span className="value">{data.energy}%</span>
17      </div>
18      <div className="stat">
19        <span className="label">Efficacité du système</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' // Toujours des données fraîches
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}

Avantages des Parallel Routes

  1. Chargement indépendant - chaque slot peut être chargé indépendamment avec son propre état de chargement
  2. Erreurs indépendantes - une erreur dans un slot n'affecte pas les autres
  3. Navigation sélective - tu peux naviguer dans un slot sans affecter les autres
  4. Rendu conditionnel - tu peux afficher/masquer des slots de manière conditionnelle

Default.tsx - Gérer les slots manquants

Lorsqu'un utilisateur navigue d'une manière qui ne correspond pas à la structure des slots (par exemple s'il recharge la page à une adresse que le slot ne gère pas), Next.js utilise le fichier default.tsx. Dans Next.js 16, ce fichier est obligatoire dans chaque slot, car sans lui next build échoue :

1// app/dashboard/@analytics/default.tsx
2export default function DefaultAnalytics() {
3  return <div>Aucune donnée analytique disponible</div>;
4}

Rendu conditionnel des slots

1// app/dashboard/layout.tsx
2import { getCurrentUser } from '@/lib/auth';
3
4export default async function DashboardLayout({
5  children,
6  analytics,
7  team,
8  activity,
9}: {
10  children: React.ReactNode;
11  analytics: React.ReactNode;
12  team: React.ReactNode;
13  activity: React.ReactNode;
14}) {
15  // Le rôle vient de la session : params ne contient que les segments dynamiques de l'URL
16  const user = await getCurrentUser();
17  const isAdmin = user?.role === 'admin';
18
19  return (
20    <div className="quantum-dashboard">
21      <div className="main-content">{children}</div>
22
23      <div className="panels">
24        {analytics}
25        {team}
26        {isAdmin && activity} {/* Uniquement pour les admins */}
27      </div>
28    </div>
29  );
30}

Intercepting Routes - Interception de la navigation

Que sont les Intercepting Routes ?

Les Intercepting Routes permettent d'intercepter la navigation et d'afficher une vue alternative pendant que l'URL change. C'est comme les tunnels quantiques de Metropolis Quantum - ils permettent des raccourcis sans changer la destination finale.

Convention de nommage

Les Intercepting Routes utilisent des préfixes spéciaux :

  • (.) - intercepte les segments au même niveau
  • (..) - intercepte les segments un niveau au-dessus
  • (..)(..) - intercepte les segments deux niveaux au-dessus
  • (...) - intercepte les segments depuis la racine app/

Exemple 1 : Modale d'image

Un cas d'usage classique - une galerie de photos où le clic ouvre une modale mais l'URL change :

1app/
2├── layout.tsx
3├── page.tsx              # Galerie /
4├── photos/
5│   └── [id]/
6│       └── page.tsx      # Page photo complète /photos/123
7└── @modal/
8    ├── default.tsx       # Renvoie null quand la modale est fermée (obligatoire dans Next.js 16)
9    └── (.)photos/
10        └── [id]/
11            └── page.tsx  # Modale d'interception /photos/123

@modal est un slot et non un segment de l'URL : les photos de app/photos sont donc interceptées par (.)photos. Écrire (..)photos à ce niveau fait échouer next build avec « Cannot use (..) marker at the root level, use (.) instead ».

1// app/page.tsx - Galerie
2import Link from 'next/link';
3import Image from 'next/image';
4
5const photos = [
6  { id: '1', url: '/photos/quantum-city-1.jpg', title: 'Quantum District' },
7  { id: '2', url: '/photos/quantum-city-2.jpg', title: 'Central Plaza' },
8  { id: '3', url: '/photos/quantum-city-3.jpg', title: 'Night Skyline' },
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 - Page photo complète
2import Image from 'next/image';
3
4async function getPhoto(id: string) {
5  // Récupère les données de la photo
6  return {
7    id,
8    url: `/photos/quantum-city-${id}.jpg`,
9    title: 'Quantum City',
10    description: 'Vue de Metropolis 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 - Modale
2'use client';
3
4import { useRouter } from 'next/navigation';
5import Image from 'next/image';
6import { use, useEffect, useState } from 'react';
7
8export default function PhotoModal({
9  params,
10}: {
11  params: Promise<{ id: string }>;
12}) {
13  const { id } = use(params);
14  const router = useRouter();
15  const [photo, setPhoto] = useState(null);
16
17  useEffect(() => {
18    // Récupère les données de la photo
19    fetch(`/api/photos/${id}`)
20      .then(res => res.json())
21      .then(setPhoto);
22  }, [id]);
23
24  if (!photo) return null;
25
26  return (
27    <div
28      className="modal-overlay"
29      onClick={() => router.back()}
30    >
31      <div
32        className="modal-content"
33        onClick={(e) => e.stopPropagation()}
34      >
35        <button
36          className="modal-close"
37          onClick={() => router.back()}
38          aria-label="Fermer"
39        >
40          ×
41        </button>
42
43        <Image
44          src={photo.url}
45          alt={photo.title}
46          width={800}
47          height={600}
48        />
49
50        <h2>{photo.title}</h2>
51        <p>{photo.description}</p>
52      </div>
53    </div>
54  );
55}
1// app/layout.tsx - Mise en page avec modale
2export default function RootLayout({
3  children,
4  modal,
5}: {
6  children: React.ReactNode;
7  modal: React.ReactNode;
8}) {
9  return (
10    <html lang="fr">
11      <body>
12        {children}
13        {modal}
14      </body>
15    </html>
16  );
17}

Le slot @modal a aussi besoin d'un fichier default.tsx qui n'affiche rien quand la modale est fermée :

1// app/@modal/default.tsx
2export default function Default() {
3  return null;
4}

Exemple 2 : Modale de connexion avec Intercepting Route

1app/
2├── layout.tsx
3├── page.tsx
4├── login/
5│   └── page.tsx          # Page de connexion complète /login
6└── @auth/
7    ├── default.tsx       # Renvoie null quand la modale est fermée
8    └── (.)login/
9        └── page.tsx      # Modale de connexion
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    // Logique de connexion
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>Connecte-toi à Metropolis 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="Mot de passe"
38            required
39          />
40
41          <button type="submit">Se connecter</button>
42        </form>
43      </div>
44    </div>
45  );
46}

Le root layout doit aussi recevoir la prop auth et afficher {auth}, comme {modal} dans l'exemple 1.

Avantages des Intercepting Routes

  1. Meilleure UX - des modales rapides sans rechargement complet de la page
  2. URL partageables - l'URL change, donc le lien peut être partagé
  3. Fallback élégant - un accès direct à l'URL affiche la page complète
  4. Préservation du contexte - l'arrière-plan reste visible derrière la modale

Route Groups - Organisation des routes

Que sont les Route Groups ?

Les Route Groups permettent de regrouper logiquement les routes sans affecter la structure de l'URL. C'est comme les districts de Metropolis Quantum - ils organisent la ville mais n'affectent pas les adresses des bâtiments.

Convention de nommage - (folder)

Les Route Groups utilisent des parenthèses dans les noms de dossiers :

1app/
2├── (marketing)/        # Groupe - n'affecte pas l'URL
3│   ├── layout.tsx      # Mise en page marketing
4│   ├── about/
5│   │   └── page.tsx    # URL : /about
6│   └── contact/
7│       └── page.tsx    # URL : /contact
8├── (shop)/            # Groupe - n'affecte pas l'URL
9│   ├── layout.tsx      # Mise en page boutique
10│   ├── products/
11│   │   └── page.tsx    # URL : /products
12│   └── cart/
13│       └── page.tsx    # URL : /cart
14└── (dashboard)/       # Groupe - n'affecte pas l'URL
15    ├── layout.tsx      # Mise en page dashboard
16    ├── analytics/
17    │   └── page.tsx    # URL : /analytics
18    └── settings/
19        └── page.tsx    # URL : /settings

Exemple 1 : Différentes mises en page pour différentes sections

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}

Exemple 2 : Organisation par fonctionnalité

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    ├── layout.tsx       # Layout commun du groupe
17    ├── dashboard/
18    │   └── page.tsx     # /dashboard
19    └── profile/
20        └── page.tsx     # /profile

Remarque que le groupe (protected) n'a pas de fichier proxy.ts : il ne peut y avoir qu'un seul proxy (middleware.ts dans Next.js 15 et les versions précédentes), à la racine du projet. Tu protèges donc ces routes avec un matcher dans le proxy.ts racine et en vérifiant la session au plus près des données.

Exemple 3 : Plusieurs mises en page racine

Les Route Groups permettent de créer plusieurs mises en page racine dans la même application :

1app/
2├── (shop)/
3│   ├── layout.tsx       # Mise en page racine pour la boutique
4│   └── products/
5│       └── page.tsx
6└── (blog)/
7    ├── layout.tsx       # Mise en page racine pour le blog
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}

Avantages des Route Groups

  1. Organisation du code - regroupement logique sans affecter l'URL
  2. Différentes mises en page - possibilité d'appliquer différentes mises en page à différentes sections
  3. Meilleure structure de projet - gestion plus simple des grandes applications
  4. Séparation des responsabilités - différentes équipes peuvent travailler sur différents groupes

Combiner les trois techniques

La véritable puissance de l'App Router se révèle lorsque tu combines les trois techniques ensemble :

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

Exemple : E-commerce avec routage avancé

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>Ajouter au panier</button>
19    </div>
20  );
21}
1// app/(shop)/@cart/(.)products/[id]/page.tsx
2'use client';
3
4import { use } from 'react';
5import { useRouter } from 'next/navigation';
6
7export default function QuickViewModal({
8  params,
9}: {
10  params: Promise<{ id: string }>;
11}) {
12  const { id } = use(params);
13  const router = useRouter();
14
15  return (
16    <div className="modal-overlay" onClick={() => router.back()}>
17      <div className="quick-view" onClick={(e) => e.stopPropagation()}>
18        <ProductQuickView productId={id} />
19        <button onClick={() => router.back()}>Fermer</button>
20      </div>
21    </div>
22  );
23}

Les deux modales sont des Client Components : elles ne peuvent donc pas utiliser await. Depuis Next.js 15, params est une promesse, et Next.js 16 ne permet plus de la lire de façon synchrone : c'est pourquoi nous la déballons avec le hook use de React.

Bonnes pratiques

1. Utilise les Parallel Routes pour des sections indépendantes

1// Bien - panneaux indépendants
2app/dashboard/
3├── @analytics/
4├── @team/
5└── @notifications/
6
7// Mal - composants dépendants
8app/dashboard/
9├── @header/      // Cela devrait être un composant, pas un slot
10├── @footer/      // Cela devrait être un composant, pas un slot
11└── @content/

2. Utilise les Intercepting Routes pour les modales et les overlays

1// Bien
2app/
3├── photos/[id]/page.tsx
4└── @modal/(.)photos/[id]/page.tsx
5
6// Mal - modale en tant que route séparée
7app/
8├── photos/[id]/page.tsx
9└── modal/photos/[id]/page.tsx

3. Utilise les Route Groups pour l'organisation

1// Bien - regroupement logique
2app/
3├── (marketing)/
4├── (dashboard)/
5└── (auth)/
6
7// Mal - regroupement par type de fichier
8app/
9├── (pages)/
10├── (components)/
11└── (api)/

Résumé

Les techniques de routage avancées dans Next.js 16 sont comme le réseau de transport avancé de Metropolis Quantum - elles permettent de créer des interfaces utilisateur complexes, efficaces et élégantes :

  1. Parallel Routes - rendre plusieurs routes simultanément pour des sections indépendantes
  2. Intercepting Routes - intercepter la navigation pour une meilleure UX avec des modales et des overlays
  3. Route Groups - organiser le code sans affecter l'URL

Ces techniques te donnent un contrôle incroyable sur la structure de l'application et l'expérience utilisateur. Tout comme les ingénieurs de Metropolis Quantum conçoivent des systèmes de transport multi-niveaux pour une efficacité maximale, tu peux toi aussi concevoir des structures de routage avancées pour une UX optimale.

La clé du succès est de comprendre quand utiliser chaque technique et comment les combiner pour créer une application Next.js 16 cohérente et efficace.

Code de cette leçon : App.tsx
1import React, { useState } from 'react';
2
3// Simulation de app/layout.tsx avec les slots @feed et @stats et la route d'interception @modal/(.)photos/[id]
4const photos = [
5  { id: '1', title: 'District quantique' },
6  { id: '2', title: 'Place centrale' },
7  { id: '3', title: 'Skyline de nuit' },
8];
9
10const panel = { flex: 1, padding: '12px', border: '1px solid #64ffda', borderRadius: '8px' };
11const button = { margin: '4px', padding: '6px 10px', background: '#1a1a2e', color: '#64ffda', border: '1px solid #64ffda', borderRadius: '6px', cursor: 'pointer' };
12
13export default function ParallelInterceptingDemo() {
14  const [url, setUrl] = useState('/');
15  const [modalId, setModalId] = useState<string | null>(null);
16
17  // Un clic sur Link (navigation dans l'application) : la route est interceptée par @modal/(.)photos/[id]
18  const openPhoto = (id: string) => { setUrl('/photos/' + id); setModalId(id); };
19  // Un rechargement de la page (F5) : pas d'interception, la page complète app/photos/[id]/page.tsx s'affiche
20  const refresh = () => setModalId(null);
21  // router.back() : retour à la galerie
22  const back = () => { setUrl('/'); setModalId(null); };
23
24  const fullPage = url !== '/' && modalId === null;
25  const photo = photos.find((p) => '/photos/' + p.id === url);
26
27  return (
28    <div style={{ fontFamily: 'monospace', background: '#0f0f23', color: '#e0e0e0', minHeight: '100vh', padding: '16px' }}>
29      <p>Adresse : <strong>{url}</strong></p>
30      {fullPage && photo ? (
31        <div style={panel}>
32          <h2>Page complète : app/photos/[id]/page.tsx</h2>
33          <p>{photo.title}</p>
34          <button style={button} onClick={back}>Retour à la galerie</button>
35        </div>
36      ) : (
37        <div style={{ display: 'flex', gap: '12px' }}>
38          <div style={panel}>
39            <h3>Slot @feed</h3>
40            {photos.map((p) => (
41              <button key={p.id} style={button} onClick={() => openPhoto(p.id)}>{p.title}</button>
42            ))}
43          </div>
44          <div style={panel}>
45            <h3>Slot @stats</h3>
46            <p>Photos dans la galerie : {photos.length}</p>
47            <p>Les deux slots s'affichent en même temps dans le même layout.</p>
48          </div>
49        </div>
50      )}
51      {modalId && photo && (
52        <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.7)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
53          <div style={{ ...panel, flex: 'none', background: '#1a1a2e', maxWidth: '320px' }}>
54            <h3>Modale de @modal/(.)photos/[id]</h3>
55            <p>{photo.title}</p>
56            <p>La galerie est restée en dessous et l'adresse est devenue {url}.</p>
57            <button style={button} onClick={back}>Fermer (router.back)</button>
58            <button style={button} onClick={refresh}>Recharger la page (F5)</button>
59          </div>
60        </div>
61      )}
62    </div>
63  );
64}

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. À quoi sert le fichier default.tsx dans les Parallel Routes ?

  2. 2. Quelle notation permet d’intercepter un segment de route un niveau au-dessus ?

Tâches pratiques dans le jeu

  • Éditeur de code

    Construis un layout avec deux slots parallèles @main et @sidebar : exporte par défaut un composant DashboardLayout({ main, sidebar }) qui affiche les deux slots côte à côte dans un seul conteneur en display: flex ou grid (par ex. un div avec style={{ display: 'flex' }} ou la classe flex).

  • Éditeur de code

    Combine plusieurs fonctionnalités de l'App Router dans un seul fichier, app/products/[id]/page.tsx, étape par étape : 1) export async function generateMetadata({ params }) renvoie { title: 'Produit ' + id } ; 2) export default async function ProductPage({ params }) lit id avec await params ; 3) si id ne contient pas uniquement des chiffres, appelle notFound() de next/navigation ; 4) sinon, renvoie un élément main avec un titre h1 « Produit {id} » et un composant Link (next/link) qui mène à /products.

Articles utiles