Chapitre 1019 minIntermédiaire
React Router : navigation, routes et paramètres d'URL
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- déclarer les routes d'une application monopage avec une mise en page commune ;
- naviguer avec Link et NavLink, et par le code avec useNavigate ;
- lire les paramètres de l'URL et y placer un filtre ;
- gérer la page introuvable et les erreurs.
4 exercices corrigés1 schéma
React Router est la bibliothèque de routage la plus utilisée avec React : elle associe chaque adresse de l'application (/, /fiches/12, /connexion) à un composant, et change d'écran sans recharger la page quand l'utilisateur clique sur un lien. Ce chapitre installe React Router 8, décrit les routes d'AppliFrais avec une mise en page commune, lit les paramètres de l'URL, gère la page 404 et la navigation par le code.
Pourquoi un routeur
Une application monopage n'a qu'une page HTML (chapitre 1). Sans routeur, l'adresse dans la barre du navigateur ne change jamais : impossible de mettre une fiche en favori, d'envoyer son lien à un collègue, d'utiliser le bouton Précédent. Le routeur :
- lit l'URL et affiche le composant correspondant ;
- intercepte les clics sur ses liens : il change l'URL avec l'API History du navigateur, sans requête au serveur, et affiche le nouvel écran ;
- réagit aux boutons Précédent et Suivant ;
- donne aux composants les paramètres de l'URL (
12dans/fiches/12).
Installer React Router 8
npm install react-router
React Router 8 est sorti en juin 2026. Il demande React 19.2 au minimum et Node.js 22.22 au minimum pour les outils. Par rapport à la version 7, le changement visible est la disparition du paquet react-router-dom : tout s'importe de react-router, sauf le RouterProvider, qui vient de react-router/dom.
React Router propose trois modes :
| Mode | Principe | Pour qui |
|---|---|---|
| Déclaratif | Des composants <BrowserRouter>, <Routes>, <Route> dans le JSX | Les projets existants, les cas très simples |
| Données (data mode) | Les routes sont décrites dans un tableau passé à createBrowserRouter | Les applications monopages : c'est le mode de ce cours |
| Framework | Un plugin Vite, des fichiers de routes, le rendu côté serveur | Les sites qui ont besoin de rendu serveur ; l'ancien Remix |
Déclarer les routes
En mode données, on décrit les routes dans un tableau d'objets, chacun avec un path (le chemin) et un element (ce qu'il affiche) :
// src/router.tsx
import { createBrowserRouter } from 'react-router';
import { Layout } from './pages/Layout';
import { PageConnexion } from './pages/PageConnexion';
import { PageFiches } from './pages/PageFiches';
import { PageFiche } from './pages/PageFiche';
import { PageIntrouvable } from './pages/PageIntrouvable';
export const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ index: true, element: <PageFiches /> },
{ path: 'fiches/:id', element: <PageFiche /> },
{ path: 'connexion', element: <PageConnexion /> },
{ path: '*', element: <PageIntrouvable /> },
],
},
]);
Puis on remplace <App /> par le fournisseur du routeur dans main.tsx :
// src/main.tsx (extrait)
import { RouterProvider } from 'react-router/dom';
import { router } from './router';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</StrictMode>,
);
Les pages sont des composants ordinaires, rangés dans src/pages/. Le fichier App.tsx n'est plus utilisé : supprime-le.
| Route | Adresse | Affiche |
|---|---|---|
index: true | / exactement | La liste des fiches |
path: 'fiches/:id' | /fiches/12, /fiches/13… | Le détail d'une fiche ; :id est un paramètre |
path: 'connexion' | /connexion | Le formulaire de connexion |
path: '*' | Toute autre adresse | La page « introuvable » |
Les chemins des routes enfants sont relatifs au parent ('fiches/:id', sans barre oblique au début). Une route index est l'enfant affiché quand l'adresse correspond exactement au parent.
La mise en page commune : routes imbriquées et Outlet
Toutes les pages partagent un en-tête et une navigation. Plutôt que de les répéter, la route parente affiche un composant Layout, et chaque page s'affiche à l'intérieur, à l'emplacement d'un <Outlet /> :
// src/pages/Layout.tsx
import { NavLink, Outlet } from 'react-router';
export function Layout() {
return (
<>
<a href="#contenu" className="lien-evitement">Aller au contenu</a>
<header>
<p className="logo">AppliFrais</p>
<nav aria-label="Navigation principale">
<NavLink to="/" end>Mes fiches</NavLink>
</nav>
</header>
<main id="contenu" tabIndex={-1}>
<Outlet />
</main>
</>
);
}
<Outlet /> est remplacé par l'élément de la route enfant qui correspond à l'URL : PageFiches sur /, PageFiche sur /fiches/12. Le Layout reste affiché quand on change de page : seul le contenu de <main> change. Le lien d'évitement et tabIndex={-1} servent à l'accessibilité (chapitre 14).
Naviguer avec Link et NavLink
Pour un lien interne, on utilise <Link to="…">, jamais <a href="…"> :
import { Link } from 'react-router';
<Link to={`/fiches/${fiche.id}`}>Fiche de frais – {formatMois(fiche.mois)}</Link>
Link produit une vraie balise <a href="/fiches/12"> (accessible, ouvrable dans un nouvel onglet avec un clic du milieu), mais intercepte le clic normal pour changer d'écran sans recharger. Un <a href> ordinaire rechargerait toute l'application : l'état et le cache seraient perdus.
NavLink est un Link qui sait s'il correspond à la page actuelle. Il ajoute alors la classe CSS active et l'attribut aria-current="page", que les lecteurs d'écran annoncent :
<NavLink to="/" end>Mes fiches</NavLink>
nav a.active { font-weight: bold; border-bottom: 2px solid; }
L'option end dit que le lien n'est actif que sur / exactement : sans elle, il serait actif sur toutes les adresses, puisqu'elles commencent toutes par /.
Lire les paramètres de l'URL : useParams
Dans la page de détail, le hook useParams donne la valeur des paramètres déclarés dans le chemin :
// src/pages/PageFiche.tsx
import { Link, useParams } from 'react-router';
import { BadgeEtat } from '../components/BadgeEtat';
import { FormulaireFrais } from '../components/FormulaireFrais';
import { ListeFrais } from '../components/ListeFrais';
import { useFiche } from '../hooks/useFiches';
import { formatMois } from '../format';
export function PageFiche() {
const { id } = useParams();
const { data: fiche, isPending, isError, error } = useFiche(Number(id));
if (isPending) return <p role="status">Chargement de la fiche…</p>;
if (isError) return <p role="alert">{error.message}</p>;
const modifiable = fiche.etat === 'CR';
return (
<>
<p><Link to="/">← Retour à mes fiches</Link></p>
<h1>Fiche de frais – {formatMois(fiche.mois)}</h1>
<BadgeEtat etat={fiche.etat} />
<ListeFrais frais={fiche.fraisHorsForfait} modifiable={modifiable} />
{modifiable && <FormulaireFrais ficheId={fiche.id} />}
</>
);
}
Les paramètres sont toujours des chaînes ('12'), ou undefined : on convertit avec Number(id). Une adresse comme /fiches/abc donne NaN, et l'API répond 404 : la page affiche le message d'erreur, pas un écran blanc.
ListeFrais reçoit modifiable : les boutons « Supprimer » et le formulaire n'apparaissent que pour une fiche en cours de saisie. C'est du confort pour l'utilisateur ; l'API refuse de toute façon de modifier une fiche clôturée (409).
Mettre un filtre dans l'URL : useSearchParams
Au chapitre 9, le filtre par état était un useState : en rechargeant la page ou en partageant le lien, on perdait le filtre. Le placer dans la chaîne de requête de l'URL (/?etat=CL) le rend partageable et compatible avec le bouton Précédent. useSearchParams fonctionne comme useState, mais lit et écrit l'URL :
// src/pages/PageFiches.tsx (extrait)
import { useSearchParams } from 'react-router';
export function PageFiches() {
const [params, setParams] = useSearchParams();
const etat = (params.get('etat') ?? '') as EtatFiche | '';
const { data: fiches, isPending, isError, error, refetch } = useFiches(etat || undefined);
function changerEtat(nouvel: EtatFiche | '') {
setParams(nouvel ? { etat: nouvel } : {});
}
return (
<>
<h1>Mes fiches de frais</h1>
<FiltreEtat valeur={etat} onChange={changerEtat} />
{/* … chargement, erreur, liste vide, liste : comme au chapitre 9 */}
</>
);
}
Choisir « Validées » change l'adresse en /?etat=VA ; la clé de requête change, TanStack Query charge les fiches validées. Le bouton Précédent revient à / et à toutes les fiches.
Naviguer par le code : useNavigate et Navigate
Après une action, on doit parfois changer de page sans clic sur un lien : revenir à la liste après une suppression, aller à la page demandée après la connexion. useNavigate renvoie une fonction de navigation :
import { useNavigate } from 'react-router';
const navigate = useNavigate();
async function supprimerFiche() {
await supprimer(fiche.id);
navigate('/', { replace: true }); // replace : la fiche supprimée ne reste pas dans l'historique
}
navigate(-1) revient en arrière comme le bouton Précédent. Pour rediriger pendant le rendu (une page qui n'a pas lieu d'être affichée), on renvoie le composant <Navigate> :
import { Navigate } from 'react-router';
if (utilisateur) return <Navigate to="/" replace />; // déjà connecté : pas de page de connexion
On n'appelle jamais navigate() directement dans le corps d'un composant : c'est un effet de bord, interdit pendant le rendu. Dans le corps, <Navigate> ; dans un gestionnaire, navigate().
La page 404 et les erreurs
La route path: '*' attrape toutes les adresses qui ne correspondent à aucune autre :
// src/pages/PageIntrouvable.tsx
import { Link } from 'react-router';
export function PageIntrouvable() {
return (
<>
<h1>Page introuvable</h1>
<p>Cette adresse ne correspond à aucune page. <Link to="/">Retour à l'accueil</Link></p>
</>
);
}
Placée parmi les enfants de Layout, elle s'affiche avec l'en-tête et la navigation : l'utilisateur n'est pas perdu.
Si un composant lève une exception pendant le rendu, React Router l'attrape et affiche une page d'erreur par défaut (en développement, avec la pile d'appels). On peut la remplacer en ajoutant errorElement: <PageErreur /> à une route ; PageErreur lit l'erreur avec le hook useRouteError(). C'est la « frontière d'erreur » que fournit le routeur.
Charger le code des pages à la demande
Par défaut, Vite met tout le code dans un seul fichier JavaScript : la page de connexion télécharge aussi le code de l'écran du comptable. Pour une application qui grossit, on peut charger le code d'une route seulement quand on y va, avec l'option lazy :
{
path: 'validation',
lazy: async () => {
const { PageValidation } = await import('./pages/PageValidation');
return { Component: PageValidation };
},
},
L'import() dynamique crée un fichier séparé à la construction. Pour AppliFrais et ses quatre écrans, le gain est négligeable ; retiens que la technique existe, c'est un argument de performance et d'écoconception à l'oral.
Exercices
Exercice 1 sur 4 Associer les adresses Réussi À revoir
Avec le routeur de ce chapitre, quel composant s'affiche dans l'Outlet pour chaque adresse, et que vaut useParams() ?
//fiches/12/fiches/12/frais/?etat=CL/Fiches/12
Voir la réponseMasquer la réponse de l'exercice 1
PageFiches;useParams()vaut{}.PageFiche;{ id: '12' }(une chaîne).PageIntrouvable: aucune route n'a trois segments.useParams()vaut{ '*': 'fiches/12/frais' }, la partie attrapée par l'étoile.PageFiches: la chaîne de requête ne compte pas pour choisir la route.useSearchParams()donneetat = 'CL'.PageFiche: React Router ne tient pas compte de la casse des chemins par défaut. (Évite tout de même les majuscules dans tes adresses.)
Tu avais trouvé ?
Exercice 2 sur 4 Ajouter une route Réussi À revoir
Ajoute une page « Mon profil » à l'adresse /profil, qui affiche le composant PageProfil, et un lien dans la navigation qui est en gras quand on est sur cette page.
Voir la réponseMasquer la réponse de l'exercice 2
Dans router.tsx, parmi les enfants de Layout :
{ path: 'profil', element: <PageProfil /> },
Dans Layout.tsx, dans le <nav> :
<NavLink to="/profil">Mon profil</NavLink>
La classe active est ajoutée automatiquement sur /profil ; la règle CSS nav a.active { font-weight: bold; } met le lien en gras. Pas besoin de end : aucune autre adresse ne commence par /profil.
Tu avais trouvé ?
Exercice 3 sur 4 Corriger la navigation Réussi À revoir
Trois erreurs. Explique leurs conséquences et corrige-les.
function CarteFiche({ fiche }: CarteFicheProps) {
const navigate = useNavigate();
if (fiche.etat === 'MP') navigate('/archives');
return (
<article>
<a href={`/fiches/${fiche.id}`}>{fiche.mois}</a>
<button onClick={navigate('/fiches/' + fiche.id)}>Ouvrir</button>
</article>
);
}
Voir la réponseMasquer la réponse de l'exercice 3
navigate('/archives')dans le corps du composant : navigation pendant le rendu, interdite (React Router l'ignore et affiche un avertissement). Pour rediriger pendant le rendu,return <Navigate to="/archives" />. Mais une carte qui redirige toute la page n'a pas de sens : la liste devrait plutôt filtrer les fiches mises en paiement.<a href>recharge toute l'application à chaque clic : cache et état perdus. Utiliser<Link to={…}>.onClick={navigate(…)}appellenavigatependant le rendu (la même erreur qu'au chapitre 4) : il fautonClick={() => navigate(…)}. Et un bouton qui mène à une autre page devrait être un lien.
function CarteFiche({ fiche }: CarteFicheProps) {
return (
<article>
<Link to={`/fiches/${fiche.id}`}>{fiche.mois}</Link>
</article>
);
}
Tu avais trouvé ?
Exercice 4 sur 4 Le filtre dans l'URL Réussi À revoir
Un utilisateur filtre ses fiches sur « Clôturées », ouvre la fiche 11, puis clique sur le bouton Précédent du navigateur. Que voit-il avec la version useState du filtre (chapitre 9) ? Et avec la version useSearchParams ? Pourquoi ?
Voir la réponseMasquer la réponse de l'exercice 4
Avec useState, il revient sur toutes les fiches : en quittant la liste, le composant PageFiches a été démonté, son état perdu ; au retour, il repart de la valeur initiale ''.
Avec useSearchParams, il revient sur /?etat=CL et voit les fiches clôturées : le filtre est dans l'adresse, que l'historique du navigateur a gardée. Grâce au cache de TanStack Query, la liste s'affiche même instantanément.
Tu avais trouvé ?