Retour au cours

frontend / nextjs

Hooks de navigation

Leçon 41 exercice

Explication

Ce que vous allez apprendre

  • Lire le chemin actuel côté client avec usePathname()
  • Lire et manipuler la query string avec useSearchParams()
  • Récupérer les segments dynamiques de la route côté client avec useParams()
  • Naviguer par code avec useRouter() : push, replace, back, refresh
  • Choisir entre push et replace selon l'effet voulu sur l'historique du navigateur
  • Éviter le re-render excessif provoqué par useSearchParams() sur une grosse page

Dans quel contexte ?

Une page de résultats de recherche /produits?q=chaussures&categorie=sport doit mettre à jour l'URL à chaque frappe dans la barre de recherche, sans recharger toute la page, tout en gardant un menu qui surligne la catégorie active. Après une connexion réussie sur /login, l'application doit aussi rediriger l'utilisateur vers /dashboard sans que le bouton "retour" du navigateur ne le ramène vers le formulaire de connexion déjà soumis. Ces deux besoins précis sont couverts par les hooks de navigation de cette leçon.

À quoi servent ces hooks, d'abord

Dans une application classique (single-page app), le JavaScript client contrôle toute la navigation. Il sait quelle page est affichée, quels paramètres sont dans l'URL, etc.

Dans l'App Router, cette logique existe aussi, mais elle doit être demandée explicitement. Et seulement dans des Client Components : un Server Component n'a plus connaissance de "l'état du navigateur" une fois le HTML envoyé.

Commençons par le plus simple : connaître le chemin actuel. usePathname() renvoie le chemin actuel, par exemple /blog/mon-article — utile pour surligner le lien actif dans un menu.

Ensuite, si on a besoin des paramètres de requête. useSearchParams() donne accès à la query string (?q=...) en lecture seule, pratique pour une barre de recherche ou des filtres.

Pour les segments dynamiques de la route, il y a un troisième hook. useParams() récupère ces segments côté client — l'équivalent de la prop params qu'on utilise côté serveur.

Enfin, pour AGIR sur la navigation, pas juste la lire. useRouter() propose push (naviguer en ajoutant une entrée d'historique), replace (remplacer l'entrée actuelle, sans retour possible), back, et refresh (recharger les données serveur sans perdre l'état des composants clients).

Pourquoi choisir entre push et replace n'est pas un détail ? push empile une entrée dans l'historique, donc le bouton "retour" du navigateur y ramène. replace l'efface complètement.

Un exemple concret pour fixer l'idée. Après une connexion réussie, on préfère souvent replace : ça évite qu'un clic sur "retour" ramène l'utilisateur vers un formulaire de login déjà soumis.

MéthodeAjoute une entrée d'historiqueCas d'usage typique
router.push(url)OuiNavigation normale (clic sur un lien)
router.replace(url)NonAprès un login, une redirection définitive
router.back()Bouton "retour" personnalisé
router.refresh()NonRe-fetch les données serveur sans perdre l'état client

Bonne pratique

Isole le composant qui appelle useSearchParams() dans un petit fichier dédié (une barre de recherche, par exemple) plutôt que de l'utiliser dans un gros composant de page. Chaque changement de query string déclenche un re-render : un composant isolé limite la zone impactée.

Le piège le plus fréquent à connaître avant de coder. useSearchParams() déclenche un nouveau rendu à chaque changement de la query string. Utilisé dans un gros composant, chaque frappe dans un champ de recherche peut re-render toute une section de la page inutilement — mieux vaut l'isoler dans un petit composant dédié.

Et la suite ? Cette leçon complète la précédente : on savait définir des routes, on sait maintenant naviguer entre elles de façon dynamique côté client.

Commandes & code

Hooks de navigation

Ces hooks nécessitent "use client" : ils lisent l'état de navigation côté client.

tsx
// components/SearchBar.tsx
"use client";

import { useRouter, usePathname, useSearchParams } from "next/navigation";

export default function SearchBar() {
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();

  const currentQuery = searchParams.get("q") ?? "";

  function handleSearch(term: string) {
    const params = new URLSearchParams(searchParams.toString());
    if (term) {
      params.set("q", term);
    } else {
      params.delete("q");
    }
    // push : ajoute une entrée dans l'historique
    router.push(`${pathname}?${params.toString()}`);
  }

  return (
    <input
      defaultValue={currentQuery}
      onChange={(e) => handleSearch(e.target.value)}
      placeholder="Rechercher..."
    />
  );
}
tsx
// Navigation programmatique complète
"use client";

import { useRouter } from "next/navigation";

export default function OrderForm() {
  const router = useRouter();

  async function handleSubmit(formData: FormData) {
    const order = await createOrder(formData);

    router.push(`/orders/${order.id}`); // navigation + entrée historique
    // router.replace(`/orders/${order.id}`); // remplace l'entrée courante
    // router.back();                          // retour arrière
    // router.refresh();                       // re-fetch les Server Components sans perdre l'état client
  }

  return <form action={handleSubmit}>{/* ... */}</form>;
}

async function createOrder(formData: FormData) {
  return { id: "123" };
}
tsx
// useSelectedLayoutSegment — savoir quel enfant est actif dans un layout
"use client";

import { useSelectedLayoutSegment } from "next/navigation";
import Link from "next/link";

export default function DashboardNav() {
  const segment = useSelectedLayoutSegment(); // "settings" | "analytics" | null

  return (
    <nav>
      <Link
        href="/dashboard/settings"
        className={segment === "settings" ? "active" : ""}
      >
        Paramètres
      </Link>
      <Link
        href="/dashboard/analytics"
        className={segment === "analytics" ? "active" : ""}
      >
        Analytics
      </Link>
    </nav>
  );
}
HookUsage
useRouter()push, replace, back, refresh, prefetch
usePathname()chemin actuel, ex. /blog/mon-article
useSearchParams()URLSearchParams en lecture seule
useParams()paramètres dynamiques de la route ({ slug: "..." })

Résumé

  • Ces hooks vivent uniquement dans des Client Components ("use client").
  • router.refresh() re-fetch les données serveur sans perdre l'état des composants clients.
  • useSearchParams déclenche un re-render à chaque changement de query string : à isoler dans un composant dédié.

Exercices pratiques

1 disponible
1

Mission : le bouton retour qui ramène vers un formulaire déjà soumis

Objectif : Choisir la bonne méthode de navigation entre push et replace, et isoler un composant pour éviter des re-renders inutiles.

Contexte

Après une connexion réussie sur /login, l'application redirige avec router.push("/dashboard"). Un testeur signale qu'en cliquant sur le bouton "retour" du navigateur juste après, il revient sur /login avec le formulaire déjà soumis. Par ailleurs, la barre de recherche SearchBar (qui utilise useSearchParams()) est actuellement codée à l'intérieur du composant DashboardPage de 300 lignes, et toute la page clignote à chaque frappe.

Résoudre l’exercice →