frontend / nextjs
Hooks de navigation
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
pushetreplaceselon 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éthode | Ajoute une entrée d'historique | Cas d'usage typique |
|---|---|---|
router.push(url) | Oui | Navigation normale (clic sur un lien) |
router.replace(url) | Non | Après un login, une redirection définitive |
router.back() | — | Bouton "retour" personnalisé |
router.refresh() | Non | Re-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.
// 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..."
/>
);
}// 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" };
}// 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>
);
}| Hook | Usage |
|---|---|
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.useSearchParamsdéclenche un re-render à chaque changement de query string : à isoler dans un composant dédié.
Exercices pratiques
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.