Aller au contenu principal

React Router : une adresse par écran

Remplacer l'état « quel écran afficher ? » par de vraies adresses, et ne jamais faire confiance à ce qui est tapé dans l'URL.

Objectifs de la séance​

  • installer React Router et déclarer des routes ;
  • naviguer avec Link et useNavigate ;
  • lire un paramètre de chemin (useParams) et des paramètres de requête (useSearchParams) ;
  • valider tout ce qui vient de l'URL ;
  • afficher une page 404 pour les adresses inconnues.

Petite vérification​

Avant de commencer, vérifiez si vous savez...

  1. dire quel fichier contient le hook useQuestions et ce qu'il renvoie
  2. dans Chronometre.tsx, dire combien de hooks personnalisés sont appelés
  3. lancer npm run build et vérifier qu'il se termine sans erreur
📌 Réponse attendue

Notions théoriques​

Pourquoi un routeur ?​

Jusqu'ici, App choisissait l'écran avec un état. L'inconvénient : l'adresse du navigateur ne change jamais. Impossible de revenir à l'écran précédent avec le bouton « Précédent », de partager le lien d'une page, ou de recharger en restant au même endroit.

Un routeur associe une adresse à un composant. C'est React Router qui fait ce travail.

Installer React Router​

npm install react-router

On enveloppe ensuite toute l'application dans un BrowserRouter (dans main.tsx) :

src/main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router";
import App from "./App.tsx";
import "./index.css";

createRoot(document.getElementById("root")!).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
);

Déclarer les routes​

src/App.tsx
import { Route, Routes } from "react-router";

<Routes>
<Route path="/" element={<PageAccueil />} />
<Route path="/partie/:niveau" element={<PagePartie />} />
<Route path="/resultat" element={<PageResultat />} />
<Route path="*" element={<PageIntrouvable />} />
</Routes>
AdressePage affichée
/PageAccueil
/partie/2PagePartie (avec niveau valant "2")
/resultat?score=2&total=3PageResultat
/nimporte/quoiPageIntrouvable (la route * attrape tout le reste)

Les parties qui ne changent pas (l'en-tête) se placent en dehors de <Routes> : elles restent affichées sur toutes les pages.

Deux outils :

import { Link, useNavigate } from "react-router";

// 1) Un lien : à la place de <a href>, sans recharger la page
<Link to="/">Retour à l'accueil</Link>

// 2) Une navigation par le code, après une action (envoi d'un formulaire, fin de partie...)
const navigate = useNavigate();
navigate("/resultat?score=2&total=3");
Pas de navigate pendant le rendu

navigate(...) s'appelle dans une fonction d'événement (clic, envoi de formulaire, fin de partie), jamais directement dans le corps du composant : cela provoquerait des navigations en boucle.

Lire l'adresse : paramètres de chemin et de requête​

  • un paramètre de chemin fait partie de l'adresse : /partie/:niveau, lu avec useParams() ;
  • un paramètre de requête suit le ? : ?pseudo=Alice, lu avec useSearchParams().
const { niveau } = useParams(); // string | undefined
const [parametres] = useSearchParams();
const pseudo = parametres.get("pseudo"); // string | null

Remarquez les types : useParams renvoie string | undefined, get renvoie string | null. TypeScript vous rappelle que le paramètre peut être absent.

Tout ce qui vient de l'URL est non fiable​

N'importe qui peut taper /partie/abc, /partie/999 ou /resultat?score=-5&total=banane dans la barre d'adresse. Il faut donc valider chaque paramètre avec une petite fonction pure, comme pour les données d'un formulaire.

src/logic/niveau.ts
export function lireNiveau(valeur: string | undefined): Niveau | null {
const nombre = Number(valeur);
if (nombre === 1 || nombre === 2 || nombre === 3) {
return nombre;
}
return null;
}
src/logic/parametres.ts
export function lirePseudo(valeur: string | null): string {
const pseudo = (valeur ?? "").trim().slice(0, 20);
return pseudo === "" ? "Joueur" : pseudo;
}

export function lireEntier(valeur: string | null): number | null {
if (valeur === null || valeur.trim() === "") {
return null;
}
const nombre = Number(valeur);
return Number.isInteger(nombre) && nombre >= 0 ? nombre : null;
}

lireNiveau renvoie null quand la valeur est invalide (la page affichera « Niveau inconnu ») ; lireEntier refuse les textes vides, les décimaux et les nombres négatifs ; lirePseudo limite la longueur et propose une valeur par défaut.

Et la sécurité de l'affichage ?

Quand vous écrivez {pseudo} dans le JSX, React échappe automatiquement le texte : un pseudo comme <script>alert(1)</script> s'affichera tel quel, sans être exécuté. Le danger n'existe que si vous utilisez dangerouslySetInnerHTML : ne le faites jamais avec une donnée venue de l'extérieur.

Un score dans l'URL est-il fiable ?​

Ici, le score est passé dans l'URL pour simplifier. N'importe quel joueur peut donc écrire /resultat?score=7&total=7. Pour un exercice, ce n'est pas grave. Pour un vrai classement, le score doit être calculé par le serveur, jamais envoyé par le navigateur.


Test de mémorisation/compréhension​


Quel composant déclare l'association entre une adresse et une page ?


Que renvoie useParams() pour une route /partie/:niveau visitée via /partie/2 ?


Pourquoi faut-il valider les paramètres lus dans l'URL ?


Quand utilise-t-on useNavigate plutôt que Link ?


Que fait <Route path="*" element={<PageIntrouvable />} /> ?


Que risque-t-on si on appelle navigate("/resultat") directement dans le corps d'un composant ?



TP pour réfléchir et résoudre des problèmes​

Votre mission : transformer le quiz en application à plusieurs pages, avec des adresses validées.

Étape 1 — Installer et activer le routeur​

Installez React Router (npm install react-router), puis enveloppez App dans main.tsx.


Bonne pratique - Le routeur dans main.tsx

Placer BrowserRouter dans main.tsx garde App indépendant du type de routeur : pour tester App, on l'enveloppera simplement dans un autre routeur (tutoriel 5).

Étape 2 — Valider les paramètres​

Ajoutez lireNiveau à src/logic/niveau.ts et créez src/logic/parametres.ts (voir « Notions théoriques »). Complétez la condition de lireEntier.


Bonne pratique - Renvoyer null plutôt qu'une valeur inventée

Pour un score illisible, mieux vaut null (la page affiche « Résultat introuvable ») que 0 : un 0 inventé ferait croire à un vrai score.

Étape 3 — Les routes​

Remplacez le contenu de App.tsx : l'en-tête reste affiché, les pages changent selon l'adresse.


Bonne pratique - Prévoir la page 404

Une adresse inconnue ne doit jamais afficher un écran blanc : la route * donne un message clair et un lien de retour.

Étape 4 — La page d'accueil​

Créez src/pages/PageAccueil.tsx : à l'envoi du formulaire, on navigue vers la partie du niveau choisi, en passant le pseudo en paramètre.


Bonne pratique - URLSearchParams pour construire une requête

URLSearchParams encode correctement les caractères spéciaux (espaces, accents, &). Construire à la main "?pseudo=" + pseudo casserait l'adresse avec un pseudo comme A&B.

Étape 5 — La page de partie​

Créez src/pages/PagePartie.tsx. Elle valide le niveau, charge les questions avec useQuestions, gère les trois états, et navigue vers le résultat.


Bonne pratique - Les hooks avant les return anticipés

Tous les hooks (useParams, useNavigate, useQuestions...) sont appelés en haut, avant les return conditionnels : un hook ne doit jamais être sauté selon une condition.

Étape 6 — La page de résultat​

Créez src/pages/PageResultat.tsx, et adaptez Resultat.tsx : le bouton « Rejouer » devient un Link vers l'accueil (la prop onRejouer disparaît).


Bonne pratique - Essayer de casser son application

Tapez à la main dans la barre d'adresse : /partie/abc, /partie/9, /resultat?score=9&total=3, /resultat?score=-1&total=banane, /nimporte/quoi. Chacune doit afficher un message clair, jamais un écran blanc ni une erreur dans la console.

📌 Une solution

Ce qu'il faut retenir​

NotionRésumé
RouteurAssocie une adresse à un composant ; BrowserRouter enveloppe l'application.
Routes / RouteDéclarent les pages ; path="*" attrape les adresses inconnues.
Link / useNavigateNaviguer par un lien, ou par le code dans une fonction d'événement.
useParams / useSearchParamsLire le chemin (:niveau) et les paramètres de requête (?pseudo=).
ValidationTout paramètre d'URL est non fiable : le valider avec une fonction pure.
URLSearchParamsConstruit une requête en encodant correctement les caractères spéciaux.

Aperçu de la prochaine séance​

Le pseudo voyage dans l'URL, d'une page à l'autre : c'est pratique mais fragile. Dans la prochaine séance, vous allez le ranger dans un contexte React, accessible à tous les composants sans passer par les props ni l'adresse.