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
LinketuseNavigate; - 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...
- dire quel fichier contient le hook
useQuestionset ce qu'il renvoie - dans
Chronometre.tsx, dire combien de hooks personnalisés sont appelés - lancer
npm run buildet 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) :
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
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>
| Adresse | Page affichée |
|---|---|
/ | PageAccueil |
/partie/2 | PagePartie (avec niveau valant "2") |
/resultat?score=2&total=3 | PageResultat |
/nimporte/quoi | PageIntrouvable (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.
Naviguer
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");
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 avecuseParams(); - un paramètre de requête suit le
?:?pseudo=Alice, lu avecuseSearchParams().
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.
export function lireNiveau(valeur: string | undefined): Niveau | null {
const nombre = Number(valeur);
if (nombre === 1 || nombre === 2 || nombre === 3) {
return nombre;
}
return null;
}
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.
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
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.
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.
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.
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.
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.
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).
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
| Notion | Résumé |
|---|---|
| Routeur | Associe une adresse à un composant ; BrowserRouter enveloppe l'application. |
Routes / Route | Déclarent les pages ; path="*" attrape les adresses inconnues. |
Link / useNavigate | Naviguer par un lien, ou par le code dans une fonction d'événement. |
useParams / useSearchParams | Lire le chemin (:niveau) et les paramètres de requête (?pseudo=). |
| Validation | Tout paramètre d'URL est non fiable : le valider avec une fonction pure. |
URLSearchParams | Construit 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.