Cours Next.js · Module 2: Routing et layouts
Routes imbriquées et paramètres catch-all
Dans cette leçon10
À Metropolis Quantum, le système de transport permet aux habitants d'atteindre chaque recoin de la ville, et même les territoires inexplorés. Des véhicules spéciaux, les Q-Hopper, peuvent collecter plusieurs destinations de voyage et les visiter toutes l'une après l'autre, en s'adaptant dynamiquement aux besoins des passagers. Cette flexibilité est essentielle au fonctionnement d'une structure urbaine aussi avancée.
Dans le monde de Next.js 16, une flexibilité similaire est offerte par des fonctionnalités de routage avancées comme les routes imbriquées et les paramètres catch-all. Elles permettent de gérer des structures d'URL complexes et la génération dynamique de routes à partir de données.
Routes imbriquées dynamiques
Dans Next.js 16, on peut créer des routes imbriquées dynamiques en combinant des segments statiques et dynamiques à différents niveaux de la hiérarchie :
1app/
2 ├── destinations/ # /destinations
3 │ ├── page.tsx # Page liste des destinations
4 │ └── [destinationId]/ # Segment dynamique pour l'ID de la destination
5 │ ├── page.tsx # /destinations/:destinationId
6 │ └── attractions/ # /destinations/:destinationId/attractions
7 │ ├── page.tsx # Page des attractions de la destination
8 │ └── [attractionId] # Segment dynamique imbriqué
9 │ └── page.tsx # /destinations/:destinationId/attractions/:attractionIdDans cette structure, on a deux niveaux de segments dynamiques : [destinationId] et [attractionId]. Cela permet de créer des chemins comme /destinations/mars/attractions/olympus-mons.
Implémenter des routes imbriquées dynamiques
Voici un exemple d'implémentation d'un système de routes imbriquées pour notre application Quantum Voyages :
1// app/destinations/[destinationId]/page.tsx
2import { getDestinationById } from '@/lib/api';
3import { DestinationDetails } from '@/components/destinations/DestinationDetails';
4import { AttractionsPreview } from '@/components/destinations/AttractionsPreview';
5import Link from 'next/link';
6import { notFound } from 'next/navigation';
7
8// Génération des paramètres statiques pour les destinations les plus populaires
9export async function generateStaticParams() {
10 // Récupère les destinations les plus populaires pour le prérendu
11 const destinations = await getPopularDestinations();
12
13 return destinations.map((destination) => ({
14 destinationId: destination.id,
15 }));
16}
17
18export default async function DestinationPage({ params }: { params: Promise<{ destinationId: string }> }) {
19 const { destinationId } = await params;
20 const destination = await getDestinationById(destinationId);
21
22 // Si la destination n'existe pas, on affiche la page 404
23 if (!destination) {
24 notFound();
25 }
26
27 return (
28 <div className="container mx-auto px-4 py-12">
29 <h1 className="text-4xl font-bold mb-6">{destination.name}</h1>
30
31 <DestinationDetails destination={destination} />
32
33 <section className="mt-12">
34 <div className="flex justify-between items-center mb-6">
35 <h2 className="text-2xl font-semibold">Attractions populaires</h2>
36 <Link
37 href={`/destinations/${destinationId}/attractions`}
38 className="text-indigo-600 hover:underline"
39 >
40 Voir toutes les attractions
41 </Link>
42 </div>
43
44 <AttractionsPreview destinationId={destinationId} />
45 </section>
46 </div>
47 );
48}1// app/destinations/[destinationId]/attractions/page.tsx
2import { getDestinationById, getAttractionsForDestination } from '@/lib/api';
3import Link from 'next/link';
4import Image from 'next/image';
5import { notFound } from 'next/navigation';
6
7export default async function DestinationAttractionsPage({ params }: { params: Promise<{ destinationId: string }> }) {
8 const { destinationId } = await params;
9 const destination = await getDestinationById(destinationId);
10
11 if (!destination) {
12 notFound();
13 }
14
15 const attractions = await getAttractionsForDestination(destinationId);
16
17 return (
18 <div className="container mx-auto px-4 py-12">
19 <div className="flex items-center gap-2 mb-8 text-sm text-gray-500">
20 <Link href="/destinations" className="hover:text-indigo-600">Destinations</Link>
21 <span>/</span>
22 <Link href={`/destinations/${destinationId}`} className="hover:text-indigo-600">
23 {destination.name}
24 </Link>
25 <span>/</span>
26 <span>Attractions</span>
27 </div>
28
29 <h1 className="text-3xl font-bold mb-2">Attractions à {destination.name}</h1>
30 <p className="text-gray-600 mb-8">{destination.description}</p>
31
32 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
33 {attractions.map((attraction) => (
34 <Link
35 key={attraction.id}
36 href={`/destinations/${destinationId}/attractions/${attraction.id}`}
37 className="block group"
38 >
39 <div className="rounded-lg overflow-hidden shadow-md hover:shadow-lg transition-shadow">
40 <div className="relative h-48">
41 <Image
42 src={attraction.imageUrl}
43 alt={attraction.name}
44 fill
45 className="object-cover transition-transform group-hover:scale-105"
46 />
47 </div>
48 <div className="p-4">
49 <h2 className="text-xl font-semibold mb-2">{attraction.name}</h2>
50 <p className="text-gray-600 line-clamp-2">{attraction.shortDescription}</p>
51 </div>
52 </div>
53 </Link>
54 ))}
55 </div>
56 </div>
57 );
58}1// app/destinations/[destinationId]/attractions/[attractionId]/page.tsx
2import { getDestinationById, getAttractionById } from '@/lib/api';
3import Link from 'next/link';
4import Image from 'next/image';
5import { notFound } from 'next/navigation';
6
7export default async function AttractionPage({
8 params
9}: {
10 params: Promise<{ destinationId: string; attractionId: string }>
11}) {
12 const { destinationId, attractionId } = await params;
13
14 const [destination, attraction] = await Promise.all([
15 getDestinationById(destinationId),
16 getAttractionById(destinationId, attractionId)
17 ]);
18
19 if (!destination || !attraction) {
20 notFound();
21 }
22
23 return (
24 <div className="container mx-auto px-4 py-12">
25 <div className="flex items-center gap-2 mb-8 text-sm text-gray-500">
26 <Link href="/destinations" className="hover:text-indigo-600">Destinations</Link>
27 <span>/</span>
28 <Link href={`/destinations/${destinationId}`} className="hover:text-indigo-600">
29 {destination.name}
30 </Link>
31 <span>/</span>
32 <Link href={`/destinations/${destinationId}/attractions`} className="hover:text-indigo-600">
33 Attractions
34 </Link>
35 <span>/</span>
36 <span>{attraction.name}</span>
37 </div>
38
39 <div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
40 <div className="relative h-[400px] lg:h-[600px] rounded-lg overflow-hidden">
41 <Image
42 src={attraction.imageUrl}
43 alt={attraction.name}
44 fill
45 className="object-cover"
46 preload
47 />
48 </div>
49
50 <div>
51 <h1 className="text-4xl font-bold mb-4">{attraction.name}</h1>
52 <div className="flex items-center mb-6">
53 <span className="px-3 py-1 bg-indigo-100 text-indigo-800 rounded-full text-sm">
54 {attraction.category}
55 </span>
56 <span className="mx-4 text-gray-300">|</span>
57 <div className="flex items-center text-amber-500">
58 {/* Étoiles de notation */}
59 {Array.from({ length: 5 }).map((_, i) => (
60 <svg key={i} xmlns="http://www.w3.org/2000/svg" className={`h-5 w-5 ${i < attraction.rating ? 'fill-current' : 'text-gray-300'}`} viewBox="0 0 20 20" fill="currentColor">
61 <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
62 </svg>
63 ))}
64 <span className="ml-2 text-gray-700">{attraction.rating.toFixed(1)}</span>
65 </div>
66 </div>
67
68 <p className="text-gray-700 mb-6">{attraction.description}</p>
69
70 <div className="space-y-4 mb-8">
71 <div className="flex">
72 <div className="w-32 font-semibold">Lieu :</div>
73 <div>{attraction.location}</div>
74 </div>
75 <div className="flex">
76 <div className="w-32 font-semibold">Coût de la visite :</div>
77 <div>{attraction.price} crédits</div>
78 </div>
79 <div className="flex">
80 <div className="w-32 font-semibold">Durée de la visite :</div>
81 <div>{attraction.duration}</div>
82 </div>
83 </div>
84
85 <button className="w-full py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors">
86 Réserver une visite
87 </button>
88 </div>
89 </div>
90 </div>
91 );
92}Paramètres catch-all
À Metropolis Quantum, la technologie Q-Route permet de voyager vers un nombre quelconque d'endroits à partir d'un seul code d'itinéraire. De la même manière, dans Next.js 16, les paramètres catch-all permettent de gérer des chemins comportant un nombre quelconque de segments.
Syntaxe des paramètres catch-all
Les paramètres catch-all sont indiqués par trois points devant le nom du paramètre, entre crochets :
1[...param]Cette notation permet de faire correspondre un nombre quelconque de segments d'URL, qui sont passés sous forme de tableau au composant.
Structure des répertoires
1app/
2 ├── shop/ # /shop
3 │ └── [...categories]/ # /shop/category1
4 │ └── page.tsx # /shop/category1/category2/category3/etc.Dans cet exemple, le chemin /shop/clothing/men/shirts sera associé à [...categories] et le paramètre categories contiendra le tableau ['clothing', 'men', 'shirts'].
Implémentation pour Quantum Voyages
Implémentation d'un système de catégories de produits avec des paramètres catch-all :
1// app/shop/[...categories]/page.tsx
2import { fetchCategorizedProducts } from '@/lib/api/products';
3import { Breadcrumbs } from '@/components/shop/Breadcrumbs';
4import { ProductGrid } from '@/components/shop/ProductGrid';
5import { CategoryFilters } from '@/components/shop/CategoryFilters';
6
7export default async function CategoriesPage({ params }: { params: Promise<{ categories: string[] }> }) {
8 const { categories } = await params;
9
10 // Récupération des produits en fonction de toutes les catégories fournies
11 const { products, availableFilters } = await fetchCategorizedProducts(categories);
12
13 // Construction du titre à partir de la dernière catégorie
14 const currentCategory = categories[categories.length - 1];
15 const formattedCategoryName = currentCategory
16 .split('-')
17 .map(word => word.charAt(0).toUpperCase() + word.slice(1))
18 .join(' ');
19
20 return (
21 <div className="container mx-auto px-4 py-12">
22 <Breadcrumbs categories={categories} />
23
24 <h1 className="text-3xl font-bold mb-8">{formattedCategoryName}</h1>
25
26 <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
27 <div className="lg:col-span-1">
28 <CategoryFilters
29 categories={categories}
30 availableFilters={availableFilters}
31 />
32 </div>
33
34 <div className="lg:col-span-3">
35 <ProductGrid products={products} />
36 </div>
37 </div>
38 </div>
39 );
40}Composants d'aide
1// components/shop/Breadcrumbs.tsx
2import Link from 'next/link';
3
4export function Breadcrumbs({ categories }: { categories: string[] }) {
5 return (
6 <div className="flex items-center flex-wrap gap-2 mb-8 text-sm text-gray-500">
7 <Link href="/shop" className="hover:text-indigo-600">
8 Boutique
9 </Link>
10
11 {categories.map((category, index) => {
12 // Construction du chemin d'URL pour cette catégorie
13 const url = `/shop/${categories.slice(0, index + 1).join('/')}`;
14 const isLast = index === categories.length - 1;
15
16 const formattedName = category
17 .split('-')
18 .map(word => word.charAt(0).toUpperCase() + word.slice(1))
19 .join(' ');
20
21 return (
22 <div key={category} className="flex items-center">
23 <span className="mx-2">/</span>
24 {isLast ? (
25 <span>{formattedName}</span>
26 ) : (
27 <Link href={url} className="hover:text-indigo-600">
28 {formattedName}
29 </Link>
30 )}
31 </div>
32 );
33 })}
34 </div>
35 );
36}Paramètres catch-all optionnels
Dans Next.js 16, on peut aussi définir des paramètres catch-all optionnels à l'aide de doubles crochets :
1[[...param]]La principale différence est qu'un paramètre catch-all optionnel correspondra aussi au chemin sans aucun segment. Par exemple, app/shop/[[...categories]]/page.tsx correspondra à :
/shop(icicategoriesest absent, doncundefined, c'est pourquoi le code ci-dessous utilise|| [])/shop/clothing(categories=['clothing'])/shop/clothing/men/shirts(categories=['clothing', 'men', 'shirts'])
Implémenter une boutique avec des paramètres catch-all optionnels
1// app/shop/[[...categories]]/page.tsx
2import { fetchCategorizedProducts, fetchFeaturedProducts } from '@/lib/api/products';
3import { Breadcrumbs } from '@/components/shop/Breadcrumbs';
4import { ProductGrid } from '@/components/shop/ProductGrid';
5import { CategoryFilters } from '@/components/shop/CategoryFilters';
6import { FeaturedProducts } from '@/components/shop/FeaturedProducts';
7import { CategoryPreview } from '@/components/shop/CategoryPreview';
8
9export default async function ShopPage({ params }: { params: Promise<{ categories?: string[] }> }) {
10 const categories = (await params).categories || [];
11 const isRootShopPage = categories.length === 0;
12
13 if (isRootShopPage) {
14 // On est sur la page principale de la boutique
15 const featuredProducts = await fetchFeaturedProducts();
16 const topCategories = await fetchTopCategories();
17
18 return (
19 <div className="container mx-auto px-4 py-12">
20 <h1 className="text-4xl font-bold mb-12">Boutique Quantum Voyages</h1>
21
22 <section className="mb-16">
23 <h2 className="text-2xl font-semibold mb-8">Produits en vedette</h2>
24 <FeaturedProducts products={featuredProducts} />
25 </section>
26
27 <section>
28 <h2 className="text-2xl font-semibold mb-8">Parcourir les catégories</h2>
29 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
30 {topCategories.map(category => (
31 <CategoryPreview key={category.id} category={category} />
32 ))}
33 </div>
34 </section>
35 </div>
36 );
37 } else {
38 // On est sur une page de catégorie
39 const { products, availableFilters } = await fetchCategorizedProducts(categories);
40
41 // Construction du titre à partir de la dernière catégorie
42 const currentCategory = categories[categories.length - 1];
43 const formattedCategoryName = currentCategory
44 .split('-')
45 .map(word => word.charAt(0).toUpperCase() + word.slice(1))
46 .join(' ');
47
48 return (
49 <div className="container mx-auto px-4 py-12">
50 <Breadcrumbs categories={categories} />
51
52 <h1 className="text-3xl font-bold mb-8">{formattedCategoryName}</h1>
53
54 <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
55 <div className="lg:col-span-1">
56 <CategoryFilters
57 categories={categories}
58 availableFilters={availableFilters}
59 />
60 </div>
61
62 <div className="lg:col-span-3">
63 <ProductGrid products={products} />
64 </div>
65 </div>
66 </div>
67 );
68 }
69}Accéder aux paramètres dans les composants
Dans Next.js 16, on peut accéder aux paramètres de chemin de plusieurs façons :
Dans les Server Components
1// Server Component
2export default async function ProductPage({ params }: { params: Promise<{ productId: string }> }) {
3 // Utilisation de (await params).productId
4 return <div>ID du produit : {(await params).productId}</div>;
5}Dans les Client Components
Dans les composants côté client, on peut utiliser le hook useParams de next/navigation :
1'use client';
2
3import { useParams } from 'next/navigation';
4
5export default function ClientProductDetails() {
6 const params = useParams();
7 const productId = params.productId;
8
9 return <div>Affichage du produit : {productId}</div>;
10}Dans les fonctions de génération
Next.js 16 propose des fonctions pour générer les paramètres statiques et les métadonnées :
1// Génération des paramètres statiques
2export async function generateStaticParams() {
3 const products = await fetchTopProducts();
4
5 return products.map((product) => ({
6 productId: product.id,
7 }));
8}
9
10// Génération des métadonnées
11export async function generateMetadata({ params }: { params: Promise<{ productId: string }> }) {
12 const product = await fetchProductById((await params).productId);
13
14 return {
15 title: product.name,
16 description: product.description,
17 openGraph: {
18 images: [{ url: product.imageUrl }],
19 },
20 };
21}Validation et gestion des paramètres
Lorsqu'on travaille avec des paramètres dynamiques, il est important de les valider :
1export default async function ProductPage({ params }: { params: Promise<{ productId: string }> }) {
2 // Vérifie si productId est un UUID valide
3 if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test((await params).productId)) {
4 // Lève une erreur qui sera interceptée par le error.js le plus proche
5 throw new Error('Invalid product ID format');
6 }
7
8 const product = await fetchProductById((await params).productId);
9
10 // Si le produit n'existe pas, on affiche la page 404
11 if (!product) {
12 notFound();
13 }
14
15 return <ProductDetails product={product} />;
16}Gestion des erreurs et des ressources manquantes
Next.js 16 permet de gérer élégamment les situations où une ressource est introuvable ou une erreur se produit :
Gestion des erreurs avec error.js
Le fichier error.tsx doit être un Client Component (directive 'use client') : une frontière d'erreur React repose sur des méthodes de cycle de vie qui n'existent que dans le navigateur, et le bouton avec reset() a besoin d'un gestionnaire de clic.
1// app/products/[productId]/error.tsx
2'use client';
3
4import { useEffect } from 'react';
5
6export default function ProductError({
7 error,
8 reset,
9}: {
10 error: Error & { digest?: string };
11 reset: () => void;
12}) {
13 useEffect(() => {
14 // Tu peux journaliser l'erreur vers un service externe
15 console.error(error);
16 }, [error]);
17
18 return (
19 <div className="container mx-auto px-4 py-16 text-center">
20 <h2 className="text-2xl font-bold mb-4">Quelque chose s'est mal passé !</h2>
21 <p className="mb-8 text-gray-600">{error.message}</p>
22 <button
23 onClick={reset}
24 className="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700"
25 >
26 Réessayer
27 </button>
28 </div>
29 );
30}Gestion des ressources manquantes avec not-found.js
1// app/products/[productId]/not-found.tsx
2import Link from 'next/link';
3
4export default function ProductNotFound() {
5 return (
6 <div className="container mx-auto px-4 py-16 text-center">
7 <h2 className="text-3xl font-bold mb-4">Produit introuvable</h2>
8 <p className="mb-8 text-gray-600">
9 Désolé, nous n'avons pas trouvé le produit que tu recherches.
10 </p>
11 <Link
12 href="/products"
13 className="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700"
14 >
15 Retour à tous les produits
16 </Link>
17 </div>
18 );
19}Exemple : Système de documentation avancé
À Metropolis Quantum, un système de documentation interactif permet de naviguer à travers des hiérarchies d'informations complexes. Implémentons un système similaire dans Next.js 16 :
1app/
2 ├── docs/
3 │ └── [[...slug]]/ # Catch-all optionnel
4 │ └── page.tsx # /docs, /docs/guides, /docs/guides/routing, etc.1// app/docs/[[...slug]]/page.tsx
2import { fetchDocumentBySlug, fetchDocCategories } from '@/lib/api/documentation';
3import { DocSidebar } from '@/components/docs/DocSidebar';
4import { DocContent } from '@/components/docs/DocContent';
5import { DocBreadcrumbs } from '@/components/docs/DocBreadcrumbs';
6import { DocPagination } from '@/components/docs/DocPagination';
7import { notFound } from 'next/navigation';
8
9export default async function DocPage({ params }: { params: Promise<{ slug?: string[] }> }) {
10 const slug = (await params).slug || [];
11 const slugPath = slug.join('/');
12
13 // Récupère les catégories de documentation pour le menu latéral
14 const categories = await fetchDocCategories();
15
16 // Si aucun chemin n'est fourni, on affiche la page principale de la documentation
17 if (slug.length === 0) {
18 return (
19 <div className="container mx-auto px-4 py-12">
20 <div className="flex flex-col lg:flex-row gap-8">
21 <DocSidebar categories={categories} currentSlug="" />
22
23 <main className="flex-1">
24 <h1 className="text-4xl font-bold mb-8">Documentation Quantum Voyages</h1>
25
26 <div className="prose prose-indigo max-w-none">
27 <p>
28 Bienvenue dans le centre de documentation de Quantum Voyages. Tu y trouveras toutes les informations
29 nécessaires pour planifier et effectuer un voyage spatial, des guides de base
30 jusqu'aux instructions techniques avancées.
31 </p>
32
33 <h2>Sections populaires</h2>
34
35 <div className="grid grid-cols-1 md:grid-cols-2 gap-6 not-prose">
36 {categories.map(category => (
37 <div key={category.slug} className="border rounded-lg p-6 hover:shadow-md transition-shadow">
38 <h3 className="text-xl font-semibold mb-2">{category.title}</h3>
39 <p className="text-gray-600 mb-4">{category.description}</p>
40 <a href={`/docs/${category.slug}`} className="text-indigo-600 hover:underline">
41 Aperçu de {category.title.toLowerCase()} →
42 </a>
43 </div>
44 ))}
45 </div>
46 </div>
47 </main>
48 </div>
49 </div>
50 );
51 }
52
53 // Récupère le document à partir du chemin
54 const document = await fetchDocumentBySlug(slugPath);
55
56 // Si le document n'existe pas, on affiche la page 404
57 if (!document) {
58 notFound();
59 }
60
61 // Récupère les documents précédent et suivant pour la navigation
62 const { previousDoc, nextDoc } = await fetchAdjacentDocs(slugPath);
63
64 return (
65 <div className="container mx-auto px-4 py-12">
66 <DocBreadcrumbs slug={slug} />
67
68 <div className="flex flex-col lg:flex-row gap-8">
69 <DocSidebar categories={categories} currentSlug={slugPath} />
70
71 <main className="flex-1">
72 <h1 className="text-4xl font-bold mb-8">{document.title}</h1>
73
74 <DocContent content={document.content} />
75
76 <DocPagination previousDoc={previousDoc} nextDoc={nextDoc} />
77 </main>
78 </div>
79 </div>
80 );
81}
82
83// Génération des paramètres statiques pour les chemins de documentation populaires
84export async function generateStaticParams() {
85 const popularDocs = await fetchPopularDocs();
86
87 return popularDocs.map(doc => ({
88 slug: doc.slug.split('/'),
89 }));
90}Combiner les paramètres catch-all avec d'autres fonctionnalités
Next.js 16 permet de combiner les paramètres catch-all avec d'autres fonctionnalités de routage avancées :
1. Regrouper des routes avec des paramètres catch-all
1app/
2 ├── (dashboard)/
3 │ └── admin/
4 │ └── users/
5 │ └── [...roles]/ # /admin/users/admin/editor/etc.
6 │ └── page.tsx2. Intercepting routes avec des paramètres catch-all
1// app/(shop)/products/@modal/(.)[...categories]/page.tsx
2// Intercepte /products/category1/category2 sous forme de modale lors de la navigation depuis /products.
3// Un segment catch-all doit être le dernier du chemin : [productId] ne peut pas suivre [...categories].3. Parallel routes avec des paramètres catch-all
1app/
2 ├── trips/
3 │ ├── page.tsx # /trips
4 │ ├── @filters/
5 │ │ ├── default.tsx # Obligatoire dans Next.js 16
6 │ │ └── [...params]/ # Parallel route pour les filtres
7 │ │ └── page.tsx
8 │ └── @map/
9 │ ├── default.tsx # Obligatoire dans Next.js 16
10 │ └── [...location]/ # Parallel route pour la carte
11 │ └── page.tsxVariantes de navigation avancées avec paramètres
Navigation avec des paramètres dynamiques
1'use client';
2
3import { useRouter } from 'next/navigation';
4import { useState } from 'react';
5
6export function CategoryNavigator({ categories, subcategories }) {
7 const router = useRouter();
8 const [selectedCategoryId, setSelectedCategoryId] = useState('');
9
10 const handleCategoryChange = (categoryId) => {
11 setSelectedCategoryId(categoryId);
12 const selectedCategory = categories.find(c => c.id === categoryId);
13 router.push(`/shop/${selectedCategory.slug}`);
14 };
15
16 const handleSubcategoryChange = (categoryId, subcategoryId) => {
17 const selectedCategory = categories.find(c => c.id === categoryId);
18 const selectedSubcategory = subcategories.find(sc => sc.id === subcategoryId);
19 router.push(`/shop/${selectedCategory.slug}/${selectedSubcategory.slug}`);
20 };
21
22 return (
23 <div className="space-y-4">
24 <div>
25 <label className="block mb-2 text-sm font-medium">Catégorie</label>
26 <select
27 className="w-full p-2 border rounded"
28 onChange={(e) => handleCategoryChange(e.target.value)}
29 >
30 <option value="">Sélectionne une catégorie</option>
31 {categories.map(category => (
32 <option key={category.id} value={category.id}>
33 {category.name}
34 </option>
35 ))}
36 </select>
37 </div>
38
39 <div>
40 <label className="block mb-2 text-sm font-medium">Sous-catégorie</label>
41 <select
42 className="w-full p-2 border rounded"
43 onChange={(e) => handleSubcategoryChange(selectedCategoryId, e.target.value)}
44 >
45 <option value="">Sélectionne une sous-catégorie</option>
46 {/* Options de sous-catégorie */}
47 </select>
48 </div>
49 </div>
50 );
51}Recherche avancée avec des paramètres catch-all
1// app/search/[[...query]]/page.tsx
2import { searchProducts } from '@/lib/api/search';
3
4export default async function SearchPage({
5 params,
6 searchParams,
7}: {
8 params: Promise<{ query?: string[] }>;
9 searchParams: Promise<{ category?: string; sort?: string; filters?: string }>;
10}) {
11 const query = (await params).query || [];
12 const searchTerm = query.join(' ');
13
14 // Récupération des paramètres de requête
15 const { category, sort, filters } = await searchParams;
16
17 // Exécution de la recherche
18 const results = await searchProducts({
19 searchTerm,
20 category,
21 sort,
22 filters: filters ? JSON.parse(filters) : undefined
23 });
24
25 return (
26 <div className="container mx-auto px-4 py-12">
27 <h1 className="text-3xl font-bold mb-8">
28 {searchTerm
29 ? `Résultats de recherche pour : "${searchTerm}"`
30 : 'Recherche de produits'
31 }
32 </h1>
33
34 {/* Filtres de recherche */}
35 <SearchFilters
36 initialCategory={category}
37 initialSort={sort}
38 initialFilters={filters ? JSON.parse(filters) : {}}
39 />
40
41 {/* Résultats de recherche */}
42 <SearchResults results={results} />
43 </div>
44 );
45}Résumé
Les routes imbriquées dynamiques et les paramètres catch-all dans Next.js 16 offrent un système de routage extrêmement flexible, capable de gérer pratiquement n'importe quel scénario de navigation dans les applications web modernes :
Les paramètres dynamiques imbriqués permettent de créer des structures d'URL hiérarchiques complexes, comme
/destinations/:destinationId/attractions/:attractionId.Les paramètres catch-all (
[...param]) permettent de faire correspondre un nombre quelconque de segments d'URL, ce qui est idéal pour les systèmes de documentation, les catégories de produits et les interfaces de recherche flexibles.Les paramètres catch-all optionnels (
[[...param]]) gèrent en plus le cas où il n'y a aucun segment, ce qui permet d'utiliser le même composant pour le chemin de base et toutes ses sous-pages.La gestion des erreurs et des ressources manquantes via les fichiers
error.jsetnot-found.jsgarantit une expérience utilisateur élégante même quand des problèmes surviennent.La validation des paramètres évite les erreurs et assure la sécurité de l'application.
En combinant ces techniques avec des fonctionnalités comme la génération de paramètres statiques (generateStaticParams), on peut créer des systèmes de navigation efficaces, dynamiques et faciles à maintenir pour des applications de toute échelle et de toute complexité.
Dans la prochaine leçon, tu verras comment les layouts et les fichiers loading.tsx forment l'infrastructure permanente des pages.
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. Les Route Groups dans l'App Router se marquent avec :
2. Les segments catch-all dans l'App Router se créent avec :
Tâches pratiques dans le jeu
- Éditeur de code
Crée le fichier error.js : commence par la directive 'use client' (error.js doit être un Client Component) et exporte par défaut un composant ErrorPage({ error, reset }). Affiche un message d'erreur convivial, par ex. un titre h2 « Une erreur s'est produite ! », et un bouton « Réessayer » qui appelle reset() au clic.
- Tri vertical
Remets dans l'ordre les étapes de création d'une route dynamique dans l'App Router
- Clique dans l'ordre
Reconstitue la syntaxe de l'export par défaut d'un composant de page dans Next.js
- Éditeur de code
Crée un Client Component Counter (export par défaut) : le fichier commence par 'use client', tu crées l'état count avec useState(0), tu l'affiches dans un paragraphe p, et un bouton « +1 » augmente le compteur de 1 à chaque clic.