Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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 (12 dans /fiches/12).

Installer React Router 8

bash
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 :

ModePrincipePour qui
DéclaratifDes composants <BrowserRouter>, <Routes>, <Route> dans le JSXLes projets existants, les cas très simples
Données (data mode)Les routes sont décrites dans un tableau passé à createBrowserRouterLes applications monopages : c'est le mode de ce cours
FrameworkUn plugin Vite, des fichiers de routes, le rendu côté serveurLes 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) :

tsx
// 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 :

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.

RouteAdresseAffiche
index: true/ exactementLa liste des fiches
path: 'fiches/:id'/fiches/12, /fiches/13…Le détail d'une fiche ; :id est un paramètre
path: 'connexion'/connexionLe formulaire de connexion
path: '*'Toute autre adresseLa 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 /> :

tsx
// 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).

les routes (router.tsx)'/' → Layout (en-tête + Outlet)connexion → PageConnexionchapitre 12 : RouteProtegeeindex → PageFichesfiches/:id → PageFicheuseParams() → { id: '12' }* → PageIntrouvablel'écran pour /fiches/12frais.gsb.example/fiches/12LayoutAppliFrais · Mes fiches · Lucas Martinà la place de <Outlet /> :PageFiche← Retour à mes fichesFiche de frais – octobre 2026tableau des frais hors forfaitformulaire d'ajoutChanger de page ne remplace que la zone de l'Outlet : le Layout reste affiché.
Les routes imbriquées : Layout affiche la page enfant à la place de son Outlet

Pour un lien interne, on utilise <Link to="…">, jamais <a href="…"> :

tsx
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 :

tsx
<NavLink to="/" end>Mes fiches</NavLink>
css
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 :

tsx
// 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 :

tsx
// 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.

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 :

tsx
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> :

tsx
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 :

tsx
// 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 :

tsx
{
  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() ?

  1. /
  2. /fiches/12
  3. /fiches/12/frais
  4. /?etat=CL
  5. /Fiches/12
Voir la réponseMasquer la réponse de l'exercice 1
  1. PageFiches ; useParams() vaut {}.
  2. PageFiche ; { id: '12' } (une chaîne).
  3. PageIntrouvable : aucune route n'a trois segments. useParams() vaut { '*': 'fiches/12/frais' }, la partie attrapée par l'étoile.
  4. PageFiches : la chaîne de requête ne compte pas pour choisir la route. useSearchParams() donne etat = 'CL'.
  5. 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 :

tsx
{ path: 'profil', element: <PageProfil /> },

Dans Layout.tsx, dans le <nav> :

tsx
<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.

tsx
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
  1. 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.
  2. <a href> recharge toute l'application à chaque clic : cache et état perdus. Utiliser <Link to={…}>.
  3. onClick={navigate(…)} appelle navigate pendant le rendu (la même erreur qu'au chapitre 4) : il faut onClick={() => navigate(…)}. Et un bouton qui mène à une autre page devrait être un lien.
tsx
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é ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Partager l'état : remonter l'état, Context et hooks personnalisésRevoir le sommaire du cours