f) Le routage des écrans
Introduction au routing
Lorsqu'on parle d'une IHM, nous souhaitons généralement afficher différents écrans en réagissant aux actions des utilisateurs.
Le routage (ou routing en anglais) est ce qui rend possible l'affichage de différents écrans.
Dans les applications "old school" de type Multi-Page-Application (MPA), pour changer de page, il faudrait :
- Faire un clic sur un élément qui permet de faire une requête HTTP au serveur pour demander un page.
- Le browser fait la requête HTTP de type GET au serveur.
- L'application serveur (le backend) s'occupe du rendu de l'HTML et le renvoie au browser (le client).
- Le browser affiche cette page.
Dans les applications que nous allons développer dans ce cours, l'architecture est complètement différente. Nous développons des Single-Page-Applications (SPA), pour changer de page :
- Il faut cliquer sur un élément de la page permettant la navigation.
- Le JS exécuté dans le browser s'occupe de créer l'illusion que l'on change de page en faisant lui-même le rendu de la nouvelle page.
- Si des données sont nécessaires pour afficher la page, le JS exécuté dans le browser s'occupera de faire un "fetch" de celles-ci au format JSON (RESTful API) et générera dynamiquement l'HTML nécessaire.
Ainsi, dans une SPA, une seule page est chargée la toute première fois que l'on accède au serveur : c'est index.html
et tous les assets associés (scripts JS, les images, CSS, sons...). Par la suite, on va utiliser un router (qui se trouvera dans un script JS) qui s'occupera de faire du "Client Side Rendering" (rendu côté client de l'HTML).
Dans nos applications Vite + React + TS
, c'est le code transpilé du TS vers le JS qui s'occupera :
- d'accéder à un container présent dans la représentation mémoire des éléments HTML de la page (par exemple la
div#root
). - de mettre à jour la représentation mémoire de ce container avec les éléments HTML attendu pour la page demandé.
Ensuite, le browser n'aura plus qu'à redessiner la page sur base de la nouvelle représentation mémoire des éléments HTML de la page.
Notons que dans une MPA, on parle de "Server Side Rendering", car c'est le backend qui est responsable de la génération de l'HTML ; cela se fait souvent à l'aide d'un moteur de templating pour générer des views (par exemple via Handlebars
qui permet de générer des views dans une application Node.js
).
Navigation basique entre pages
A notre stade actuel de connaissances, nous pourrions très facilement organiser la navigation entre plusieurs page, simplement à l'aide d'une variable d'état et des gestionnaires de clics.
Pour ce tutoriel, nous allons partir d'une base de code minimaliste. Veuillez donc créer un nouveau projet Vite + React + TS
nommé routing
.
Vous ne vous souvenez plus comment faire ? Voici la commande :
bashnpm create vite@latest routing -- --template react-swc-ts
Veuillez remplacer le code de App
:
tsximport { useState } from "react";const HomePage = () => <div>Home Page</div>;const AboutPage = () => <div>About Page</div>;const ContactPage = () => <div>Contact Page</div>;const App = () => {const [currentPage, setCurrentPage] = useState("Home");const navigateTo = (page: string) => {setCurrentPage(page);};const renderPage = () => {switch (currentPage) {case "Home":return <HomePage />;case "About":return <AboutPage />;case "Contact":return <ContactPage />;default:return <HomePage />;}};return (<div><nav><button onClick={() => navigateTo("Home")}>Home</button><button onClick={() => navigateTo("About")}>About</button><button onClick={() => navigateTo("Contact")}>Contact</button></nav>{renderPage()}</div>);};export default App;export { HomePage, AboutPage, ContactPage };
Nous avons donc ici défini 3 composants React qui représentent 3 pages, et une fonction qui permet, lors d'un clic, d'afficher la page associée au bouton.
Veuillez exécuter l'application.
Tout fonctionne bien !
💭 Mais qu'est-ce qui nous manque ?
Voici ce qui fait défaut :
- Si nous faisons un refresh de la page, nous perdons la page en cours. Par exemple, si nous sommes sur
ContactPage
, nous serons redirigé versHomePage
. - Nous n'avons pas d'historique des pages visitées, nous ne pouvons pas revenir en arrière, ni en avant dans le temps.
- Nous n'avons pas une URL spécifique pour chaque écran.
Nous allons donc mettre en place en routeur afin de bénéficier de ces fonctions manquantes.
Mise en place de React Router
Introduction
React Router
est une librairie qui fournit une belle solution pour gérer la navigation dans une application React.
Sa documentation est disponible ici : https://reactrouter.com/en/main
Installation de la librairie
Dans un premier temps, il faut donc installer la librairie :
shnpm i react-router-dom
Mise en place de routes basiques
Dans un premier temps, nous allons voir comment créer la configuration la plus simple d'un router. Veuillez mettre à jour /src/main.tsx
:
tsx1import React from "react";2import ReactDOM from "react-dom/client";3import { AboutPage, ContactPage, HomePage } from "./App.tsx";4import "./index.css";5import { RouterProvider, createBrowserRouter } from "react-router-dom";67const router = createBrowserRouter([8{9path: "/",10element: <HomePage />,11},12{13path: "/about",14element: <AboutPage />,15},16{17path: "/contact",18element: <ContactPage />,19},20]);2122ReactDOM.createRoot(document.getElementById("root")!).render(23<React.StrictMode>24<RouterProvider router={router}/>25</React.StrictMode>26);
Ici, nous avons donc createBrowserRouter
qui crée une configuration pour nos 3 routes, et chaque route va afficher une page.
Ensuite, nous utilisons le RouterProvider
pour fournir la configuration du routeur à l'ensemble de l'application et pour permettre aux composants de l'application d'accéder au contexte du router.
Pour cette première utilisation du router, nous n'allons pas tenter d'optimiser la navigation. Nous allons donc intégrer à chaque page une NavBar
.
Chaque élément de navigation va utiliser <Link>
comme composant pour faire le lien avec les routes que nous avons configurées.
Veuillez mettre à jour le composant App
:
tsx1import { Link } from "react-router-dom";23const NavBar = () => (4<nav>5<Link to="/">Home</Link>6<Link to="/about">About</Link>7<Link to="/contact">Contact</Link>8</nav>9);1011const HomePage = () => (12<div>13<NavBar />14<p>Home Page</p>15</div>16);17const AboutPage = () => (18<div>19<NavBar />20<p>About Page</p>21</div>22);23const ContactPage = () => (24<div>25<NavBar />26<p>Contact Page</p>27</div>28);2930const App = () => {31return <div></div>;32};3334export default App;35export { HomePage, AboutPage, ContactPage };
Veuillez lancer votre application et vérifier que tout fonctionne. Lorsque vous cliquez sur un lien, le router détecte qu'il y a eu un changement d'état, et element
dans la configuration du router est rappelé pour la route associée, amenant au render du composant.
Utilisation de useNavigation
Si l'on souhaite se rapprocher du design initial, nous pouvons utiliser le hook useNavigation
qui offre une fonction pour naviguer vers une nouvelle route.
Veuillez mettre App
à jour :
tsx1import { useNavigate } from "react-router-dom";23const NavBar = () => {4const navigate = useNavigate();5return (6<nav>7<button onClick={() => navigate("/")}>Home</button>8<button onClick={() => navigate("/about")}>About</button>9<button onClick={() => navigate("/contact")}> Contact</button>10</nav>11);12};1314const HomePage = () => (15<div>16<NavBar />17<p>Home Page</p>18</div>19);20const AboutPage = () => (21<div>22<NavBar />23<p>About Page</p>24</div>25);26const ContactPage = () => (27<div>28<NavBar />29<p>Contact Page</p>30</div>31);3233const App = () => {34return <div></div>;35};3637export default App;38export { HomePage, AboutPage, ContactPage };
Voila, nous avons un design qui ressemble au design initial.
N'hésitez pas à tester le router :
- Faites un reload quand vous êtes dans la
ContactPage
pour voir que vous y restez bien. - Naviguez sur plusieurs pages, puis utiliser les fonctionnalités "Back" and "Forward" pour voyager dans l'historique de votre Browser.
- Vérifiez bien que l'URL dans le browser correspond bien à la page demandée.
💭 Est-ce qu'il n'y a pas quelque chose qui vous dérange dans la solution actuelle du layout de nos pages ?
Actuellement, nous intégrons une NavBar
dans chaque page. Cela signifie qu'à chaque navigation d'une page à une autre, c'est l'entièreté de la page qui doit être rendue, y compris les éléments de la Navbar
, qui pourtant ne changent pas !
Il serait intéressant de pouvoir définir un layout de tout ce qui devrait être rendu qu'une seule fois dans notre page, comme par exemple une Navbar
.
Pour ce faire, nous allons voir les "nested routes".
Nested routes
Il est possible de définir une route parent, ça serait la route "racine" ici, pour afficher le squelette de nos pages.
Ensuite, nous afficherons des routes "enfants" au sein de la route "parent". Pour indiquer où les routes "enfants" devront s'afficher chez le "parent", nous utiliserons un <Outlet>
.
Voici comment définir la route /
pour le squelette de l'application qui se trouvera dans App
, puis les 3 routes "enfants" pour les 3 pages (veuillez mettre à jour le router au sein de main.tsx
) :
tsx1const router = createBrowserRouter([2{3path: "/",4element: <App />,5children: [6{7path: "",8element: <HomePage />,9},10{11path: "about",12element: <AboutPage />,13},14{15path: "contact",16element: <ContactPage />,17},18],19},20]);
Il ne nous reste plus qu'à mettre à jour App
pour intégrer le Outlet
et pour enlever l'appel de chaque page à la NavBar
:
tsx1import { Outlet, useNavigate } from "react-router-dom";23const NavBar = () => {4const navigate = useNavigate();56return (7<nav>8<button onClick={() => navigate("/")}>Home</button>9<button onClick={() => navigate("/about")}>About</button>10<button onClick={() => navigate("/contact")}> Contact</button>11</nav>12);13};1415const HomePage = () => <p>Home Page</p>;16const AboutPage = () => <p>About Page</p>;17const ContactPage = () => <p>Contact Page</p>;1819const App = () => (20<div>21<NavBar />22<Outlet />23</div>24);2526export default App;27export { HomePage, AboutPage, ContactPage };
Nous avons là un code bien propre, et une navigation parfaitement fonctionnelle !
Il est à noter que le code serait encore plus simple si nous utilisions le composant Link
de la librairie (il suffirait de le styler pour qu'il ressemble à un bouton).
URL dynamiques
Parfois, il est intéressant qu'une même composant soit appelé sur toute une famille de routes.
Par exemple, dans le composant AboutPage
, nous souhaitons afficher une liste d'utilisateurs. Lorsque nous cliquons sur une utilisatrice ou un utilisateur, nous souhaitons faire appel à un nouveau composant UserPage
qui permettra d'afficher sa page associée avec comme url : /users/:userId
.
Veuillez mettre à jour le composant App
pour créer la UserPage
et mettre à jour AboutPage
:
tsx1import { Link, Outlet, useMatch, useNavigate } from "react-router-dom";23const NavBar = () => {4const navigate = useNavigate();56return (7<nav>8<button onClick={() => navigate("/")}>Home</button>9<button onClick={() => navigate("/about")}>About</button>10<button onClick={() => navigate("/contact")}> Contact</button>11</nav>12);13};1415const HomePage = () => <p>Home Page</p>;16const AboutPage = () => (17<div>18<h1>About Page</h1>19<h2>Authors:</h2>20{users.map((user) => (21<Link key={user.id} to={`/users/${user.id}`} style={{ display: "block" }}>22{user.name}23</Link>24))}25</div>26);27const ContactPage = () => <p>Contact Page</p>;2829const users: User[] = [30{31id: 1,32name: "John Doe",33email: "john.doe@example.com",34phone: "123-456-7890",35},36{37id: 2,38name: "Jane Smith",39email: "jane.smith@example.com",40phone: "234-567-8901",41},42{43id: 3,44name: "James Brown",45email: "james.brown@example.com",46phone: "345-678-9012",47},48];4950const UserPage = () => {51const match = useMatch("/users/:userId");52const userId = match?.params.userId;53if (!userId) return <p>User not found</p>;5455const user = users.find((user) => user.id.toString() === userId);56if (!user) return <p>User not found</p>;5758return (59<div>60<h2>{user.name}</h2>61<p>Email: {user.email}</p>62<p>Phone: {user.phone}</p>63</div>64);65};6667const App = () => (68<div>69<NavBar />70<Outlet />71</div>72);7374interface User {75id: number;76name: string;77email: string;78phone: string;79}8081export default App;82export { HomePage, AboutPage, ContactPage, UserPage };
Le composant AboutPage
contient des Link
qui pointent vers des URL qui sont /users/1
pour le premier user, /users/2
pour le user qui a l'id 2
...
Pour récupérer cette id dans la page des utilisateurs (le composant UserPage
), nous utilisons le hook useMatch("/users/:userId")
pour indiquer le segment dynamique de l'URL par une variable qui sera accessible via match.params.userId
.
Pour que tout cela fonctionne, il ne reste plus qu'à configurer le router pour cette route dynamique. Veuillez mettre à jour la configuration du router dans /src/main.tsx
:
tsx1const router = createBrowserRouter([2{3path: "/",4element: <App />,5children: [6{7path: "",8element: <HomePage />,9},10{11path: "about",12element: <AboutPage />,13},14{15path: "contact",16element: <ContactPage />,17},18{19path: "users/:userId",20element: <UserPage />,21}22],23},24]);
Veuillez vérifier que tout fonctionne bien, que vous pouvez afficher la page de James Brown
.
💭 Il est à noter que si nous n'avions pas voulu créer une nouvelle page mais plutôt afficher le détail d'un utilisateur dans le composant AboutPage
, nous aurions pour créer une route "enfant" de /about
(en utilisant un Outlet
dans AboutPage
).
Si nécessaire, vous pouvez trouver le code associé à ce tutoriel ici : routing.
Exercice 2.10 : React Router de base
Dans vos exercices précédents, vous avez créer une page pour afficher les films des cinémas UGC (/exercises/2.6
). Vous avez aussi créé une page pour afficher vos films préférés dans un autres exercice (/exercises/2.7
).
Nous vous proposons ici de créer une nouvelle application iMovies
qui s'occupera d'intégrer ces contenus et de mettre en place la navigation.
Veuillez partir d'une copie de l'exercice (/exercises/2.7
) et y intégrer le code de l'exercice (/exercises/2.6
) dans un nouveau projet nommé exercises/2.10-11-12
pour afficher :
- Un header & un footer pour chaque page
- Une navbar (à vous de choisir où la mettre)
- Une nouvelle
HomePage
qui donne quelques explications sur l'applicationiMovies
(pas besoin de la peaufiner, l'idée est juste de travailler la mise en place de la navigation). - Une
CinemaPage
qui reprend simplement le contenu de l'exercice (/exercises/2.6
). - Une
MovieListPage
qui reprend la liste de vos films selon le design de l'exercise (/exercises/2.7
).
Une fois tout fonctionnel, veuillez faire un commit avec le message suivant : new:ex2.10
.
Comment gérer l'état avec React Router ?
Il est possible que vous ayez remarqué, dans le code du router du tutoriel précédent, qu'il semble compliqué, voire impossible, de faire passer des variables d'états & des fonctions pour mettre à jour cet état, entre routes...
Pour la pizzeria, l'IHM que nous avons développée s'est terminée avec le code du tutoriel ui-library
. Néanmoins, pour la suite du cours, nous ne souhaitons pas vous imposer d'utiliser des composants de Material UI
. Dès lors, nous avons restructuré le code pour avoir quelque chose de propre, qui contient :
- que du CSS sans composants
MUI
; - deux pages :
HomePage
&AddPizzaPage
; - un router et une
NavBar
pour assurer la navigation. Cette nouvelle version de l'App se trouve dans le projetrouting-starter
.
Pour ce nouveau tutoriel, veuillez créer un nouveau projet routing-state
sur base d'un copier/coller du projet routing-starterrouting-starter
. Attention, il est normal que votre projet ne s'exécute pas car il manque la gestion de l'état.
N'hésitez pas à utiliser ce site pour télécharger le code du dossier routing-starter : https://download-directory.github.io/
Veuillez vous assurer que vous comprenez le code associé au routage des pages : main.tsx
, App
et HomePage
et AddPizzaPage
sont à bien analyser.
Pour gérer l'état entre siblings (deux pages ici, l'équivalent de deux routes), nous avons appris précédemment qu'il fallait :
- déclarer l'état et des fonctions pour mettre à jour cet état au niveau du parent ;
- passer cet état & fonctions aux enfants (les pages ici) qui vont devoir l'utiliser.
Or ici, la relation "parent/enfant" est compliquée, car :
- il y a un composant
<App>
qui contient tout le squelette de l'application, pour les 2 pages de l'application ; - il y a un composant
<Outlet>
qui s'occupe d'appeler les composants "enfants" (les pages) en fonction de la route.
Ainsi, il n'est pas vraiment possible de classiquement faire un "drill" des variables d'état et des fonctions. On ne peut pas passer les variables d'état, ainsi que les fonctions pour mettre à jour cet état, de App
vers HomePage
et AddMoviePage
.
Il existe plusieurs façon de gérer de manière élégante l'état de l'application. Ici, nous allons voir ce que React Router met à notre disposition sans devoir utiliser une nouvelle librairie.
Utilisation d'un OutletContext
Dans une route "parent", nous allons définir un contexte à l'aide du composant Outlet
. Ce contexte peut être n'importe quelle donnée ou fonction que nous souhaitons partager avec les routes "enfants".
Dans une route "enfant", nous pouvons accéder au contexte en utilisant le hook useOutletContext
.
Commençons par mettre à jour App
en y ajoutant la définition et le passage du contexte aux routes "enfants" :
tsx1const App = () => {2const [actionToBePerformed, setActionToBePerformed] = useState(false);3const [pizzas, setPizzas] = useState(defaultPizzas);45const addPizza = (newPizza: NewPizza) => {6const pizzaAdded = { ...newPizza, id: nextPizzaId(pizzas) };7setPizzas([...pizzas, pizzaAdded]);8};910const handleHeaderClick = () => {11setActionToBePerformed(true);12};1314const clearActionToBePerformed = () => {15setActionToBePerformed(false);16};1718const fullPizzaContext: PizzeriaContext = {19addPizza,20pizzas,21setPizzas,22actionToBePerformed,23setActionToBePerformed,24clearActionToBePerformed,25drinks,26};2728return (29<div className="page">30<Header31title="We love Pizza"32version={0 + 1}33handleHeaderClick={handleHeaderClick}34/>35<main>36<NavBar />37<Outlet context={fullPizzaContext} />38</main>39<Footer />40</div>41);42};
Pour que TS soit OK au niveau des types, nous avons défini un nouveau type dans /src/types.ts
:
tsinterface PizzeriaContext {pizzas: Pizza[];setPizzas: (pizzas: Pizza[]) => void;actionToBePerformed: boolean;setActionToBePerformed: (actionToBePerformed: boolean) => void;clearActionToBePerformed: () => void;drinks: Drink[];addPizza: (newPizza: NewPizza) => void;}export type { Pizza, NewPizza, Drink, PizzeriaContext };
Veuillez importer ce nouveau type dans App
.
Maintenant, nous souhaitons mettre à jour HomePage
pour récupérer, via le hook useOutletContext
, le PizzeriaContext
:
tsx1const HomePage = () => {2const {3actionToBePerformed,4clearActionToBePerformed,5pizzas,6drinks,7}: PizzeriaContext = useOutletContext();89return (10<>11<h1>Ma Pizzeria</h1>12<p>13Parce que nous aimons le JS/TS, vous pouvez cliquer sur le header pour14démarrer / stopper la musique ; )15</p>16<AudioPlayer17sound={sound}18actionToBePerformed={actionToBePerformed}19clearActionToBePerformed={clearActionToBePerformed}20/>2122<PizzaMenu pizzas={pizzas} />2324<DrinkMenu title="Nos boissons" drinks={drinks} />25</>26);27};
Puis, nous souhaitons aussi mettre à jour AddMoviePage
pour récupérer la fonction addPizza
du contexte :
tsxconst AddPizzaPage = () => {const { addPizza }: PizzeriaContext = useOutletContext();
N'oubliez pas de faire l'import de useOutletContext
et du type PizzeriaContext
.
Une fois les changements effectués, vous devriez avoir une application pleinement fonctionnelle, avec un routing moderne et une gestion élégante de l'état.
Si nécessaire, vous pouvez trouver le code associé à ce tutoriel ici : routing-state.
Exercice 2.11 : État avec un router
Veuillez continuer votre exercice précédent dans le projet existant et nommé exercises/2.10-11-12
en y intégrant une AddMoviePage
qui permette d'ajouter un film à la liste des films. Une fois un film ajouté, l'utilisateur est automatiquement redirigé vers la MovieListPage
.
Une fois tout fonctionnel, veuillez faire un commit avec le message suivant : new:ex2.11
.
Exercice 2.12 : Routes dynamiques
Veuillez continuer l'exercice précédent dans le projet existant et nommé exercises/2.10-11-12
.
Nous vous demandons :
- De mettre à jour la
HomePage
afin qu'elle affiche une liste reprenant uniquement les titres de vos films favoris (sans d'autres infos associées aux films telles que la description...). - Il doit être possible de pouvoir cliquer sur le titre d'un de vos films favoris et de naviguer vers une nouvelle
MoviePage
qui affichera toutes les infos de ce film-ci. Pour ce faire, vous devez ajouter un id à vos films, et cette id doit être visible dans l'URL quand les utilisateurs cliquent sur un titre donné dans laHomePage
.
Une fois tout fonctionnel, veuillez faire un commit avec le message suivant : new:ex2.12
.