Leçon 5 / 9
56%

Next.js 16 – Leçon 5 : Routing dynamique et navigation

Routes dynamiques avec [slug]

Une route dynamique capture une valeur variable dans l’URL. En App Router, on place le nom du paramètre entre crochets dans le nom du dossier : [slug], [id], [username]

Structure
app/
└── blog/
    └── [slug]/
        └── page.tsx   → /blog/mon-article
                       → /blog/nextjs-16
                       → /blog/n-importe-quel-slug

Dans page.tsx, récupérez le paramètre via les params. Dans Next.js 16, les params sont une Promise — il faut les await :

TypeScript app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';

export default async function PostPage({
  params,
}: {
  params: Promise<{ slug: string }>; // Promise dans Next.js 16
}) {
  const { slug } = await params; // obligatoire : await params

  const post = await getPostBySlug(slug);
  if (!post) notFound(); // affiche not-found.tsx

  return (
    <article className="max-w-3xl mx-auto py-12 px-4">
      <h1 className="text-4xl font-bold mb-6">{post.title}</h1>
      <p className="text-gray-600 leading-relaxed">{post.content}</p>
    </article>
  );
}
⚠️
Breaking change Next.js 16 : params est une Promise

Dans les versions précédentes, params était un objet synchrone. Dans Next.js 16, c’est une Promise — vous devez obligatoirement faire const { slug } = await params. Si vous migrez un projet, le codemod officiel (npx @next/codemod upgrade latest) s’en charge automatiquement.

generateStaticParams — pré-générer les routes

Exportez generateStaticParams pour que Next.js pré-génère les pages dynamiques au build. Les pages sont servies instantanément depuis un CDN sans aucun calcul à la requête.

TypeScript app/blog/[slug]/page.tsx
// Appelé au build — retourne tous les slugs à pré-générer
export async function generateStaticParams() {
  const posts = await getAllPosts();

  return posts.map((post) => ({
    slug: post.slug,
  }));
}

// Métadonnées dynamiques pour le SEO
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPostBySlug(slug);

  return {
    title: post?.title,
    description: post?.excerpt,
  };
}

Le composant Link — navigation client

Utilisez toujours <Link> de next/link pour naviguer entre les pages. Contrairement à <a>, il ne recharge pas la page et déclenche le prefetching automatique au survol.

TypeScript app/blog/page.tsx
import Link from 'next/link';

export default async function BlogPage() {
  const posts = await getPosts();

  return (
    <ul className="space-y-4">
      {posts.map((post) => (
        <li key={post.slug}>
          <Link
            href={`/blog/${post.slug}`}
            className="block p-4 border rounded-xl hover:border-gray-400 transition-colors"
          >
            <h2 className="font-semibold">{post.title}</h2>
            <p className="text-sm text-gray-500 mt-1">{post.excerpt}</p>
          </Link>
        </li>
      ))}
    </ul>
  );
}

useRouter — navigation programmatique

Pour naviguer depuis du code (après la soumission d’un formulaire, après un clic sur un bouton), utilisez le hook useRouter dans un Client Component :

TypeScript app/components/SearchForm.tsx
'use client';

import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { useState } from 'react';

export default function SearchForm() {
  const router = useRouter();
  const pathname = usePathname();         // ex: "/blog"
  const [query, setQuery] = useState('');

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    router.push(`/search?q=${query}`);  // navigation
  }

  return (
    <form onSubmit={handleSubmit} className="flex gap-2">
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Rechercher..."
        className="border rounded-xl px-4 py-2 flex-1"
      />
      <button type="submit" className="bg-gray-900 text-white px-4 py-2 rounded-xl">
        Chercher
      </button>
    </form>
  );
}

Prefetching incrémental dans Next.js 16

Next.js 16 réécrit complètement le système de prefetching. Au lieu de précharger la page entière, il utilise le prefetching incrémental : seules les parties pas encore en cache sont téléchargées. Résultat : les navigations sont instantanées avec beaucoup moins de données transférées.

TypeScript Contrôle du prefetch
// Prefetch par défaut (au survol)
<Link href="/blog">Blog</Link>

// Désactiver le prefetch (contenu derrière auth)
<Link href="/dashboard" prefetch={false}>Dashboard</Link>

// Prefetch dès l'entrée dans le viewport (agressif)
<Link href="/produit/vedette" prefetch={true}>Produit vedette</Link>
💡
Déduplication des layouts dans Next.js 16

Si 50 liens pointent vers des pages partageant le même layout, Next.js 16 télécharge le layout une seule fois (contre 50 fois avant). Pour une page avec une grille de produits, le gain est énorme : navigation quasi-instantanée et bande passante réduite de 90%.

Catch-all routes avec […slug]

Structure
app/docs/[...slug]/page.tsx

Capture :
/docs/intro           → slug = ['intro']
/docs/api/auth        → slug = ['api', 'auth']
/docs/api/v2/tokens   → slug = ['api', 'v2', 'tokens']

Exercice pratique

  • Créez app/blog/[slug]/page.tsx qui affiche un article fictif selon le slug reçu
  • Ajoutez generateStaticParams avec 3 slugs en dur ('nextjs-16', 'react', 'typescript')
  • Ajoutez generateMetadata pour avoir un <title> dynamique par article
  • Créez une liste dans app/blog/page.tsx avec des <Link> vers ces 3 articles
  • Vérifiez dans l’onglet Network des DevTools que le prefetch se déclenche au survol