Appearance
Routage
Les frameworks JavaScript modernes permettent de créer des Single Page Applications (SPA). Dans une SPA, l'ensemble du code frontend est récupéré dès l'arrivée de l'utilisateur sur le site.
Cela ne signifie pas pour autant que l'application frontend ne peut pas se découper visuellement en plusieurs pages. Dans ce chapitre, nous verrons comment développer une application comprenant différentes pages et comment permettre à l'utilisateur de naviguer entre elles grâce à un système de routage.
Installation
La mise en place d'un site multi-pages nécessite généralement l'installation d'un plugin complémentaire au framework. Cela permet d'alléger le framework dans le cas où l'on développe une application sans système de routage.
La première étape est donc d'installer une nouvelle bibliothèque sur le projet Node.js :
bash
npm install react-router-domNous pouvons ensuite passer à la définition des routes, permettant d'accéder à chacune des pages de l'application.
Création d'un schema de routes
L'objectif de notre système de routage est que l'application frontend adapte le contenu affiché en fonction de la page sur laquelle nous nous trouvons.
Chaque page doit avoir sa propre URL. Si l'utilisateur change de page, l'URL doit changer également. Inversement, si l'utilisateur indique l'URL d'une page spécifique, l'application doit afficher la page demandée. On dit que chaque page correspond à une route, principalement caractérisée par une chemin d'URL.
Pour l'exemple, imaginons une application comportant deux pages :
- Home : La page d'accueil du site, avec le chemin d'URL
/. C'est la page par défaut de notre application. - Contact : Une page montrant des informations de contact, avec le chemin d'URL
/contact.
INFO
👉 Les URLs indiquées ne comportent que la partie située après le nom de domaine du site. Si l'application est accessible sur https://my-domain.com, l'URL complète de la page Contact sera https://my-domain.com/contact.
Pour matérialiser ces pages dans l'application, nous devons créer un schéma de routes, présentant les informations sur l'accès à chacune de nos pages dans un tableau. Nous créerons ce schéma dans le fichier src/router/index :
jsx
import { createBrowserRouter } from "react-router-dom";
import Home from "../components/Home.js";
import Contact from "../components/Contact.js";
export default createBrowserRouter([
{
path: "/",
element: <Home />,
},
{
path: "/contact",
element: <Contact />,
},
]);Le schéma est fourni en argument de la fonction createBrowserRouter, qui permet d'instancier le routeur.
Chaque route vers une page est définie par un path, qui correspond à l'URL associée. Elle est ensuite définie par du code HTML correspondant au contenu de la page en question. On choisira généralement, comme ici, d'associer à chaque route un composant React.js défini dans d'autres fichiers.
Les composants Home et Contact utilisés dans le schéma sont des composants classiques :
jsx
export default function Home() {
return <h1>Welcome to Home page</h1>;
}jsx
export default function Contact() {
return <h1>Welcome to Contact page</h1>;
}Utilisation du routeur
Nous devons maintenant modifier le fichier main de notre application afin qu'il utilise le système de routage :
jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import router from "./router/index";
createRoot(document.getElementById('root')).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
)Dans le fichier main.jsx de l'application, nous avons simplement remplacé la référence vers le composant <App /> par un composant <RouterProvider />, fourni par react-router-dom. Le composant prend en props le router défini précédemment avec le schéma de routes.
Le composant <App /> ne sera désormais plus nécessaire à l'application. Le composant racine dépendra maintenant du code présent dans le champ element de la route courante.
Si nous nous rendons maintenant sur l'application, nous avons bien le contenu du composant <Home /> affiché, car l'URL courante est /. Si nous modifions l'URL pour celle de la page Contact /contact, l'application changera automatiquement le contenu de la page pour afficher le composant <Contact />.
Le système de routage est en place. Voyons maintenant comment naviguer entre les pages, autrement qu'en changeant manuellement l'URL.
Navigation
Pour naviguer d'une page à l'autre de l'application, il est possible d'utiliser une balise de lien de navigation fournie par le plugin :
jsx
import { Link } from 'react-router-dom'
export default function Contact() {
return (
<div>
<h1>Welcome to Contact page</h1>
<Link to="/contact">Go to contact page</Link>
</div>
)
}La balise fonctionne comme une balise <a>. L'utilisateur sera redirigé s'il clique sur le contenu de la balise, qui peut être du texte ou d'autres éléments HTML. La balise prend en attribut to le chemin de la route vers laquelle elle redirige.
INFO
👉 Nous pourrions être tentés de faire la même chose en utilisant directement une balise <a>. Toutefois, cette balise fait automatiquement une requête serveur pour obtenir une nouvelle page. Or, dans notre application, l'ensemble du code est récupéré initialement (SPA). La balise <a> fonctionnerait donc, mais rechargerait inutilement l'application et ses états.
Routes programmatives
Il arrive que l'on ne souhaite pas définir une route vers une page précise, mais vers ensemble de pages dont le chemin dépend d'un paramètre. C'est le cas, par exemple, si notre application propose des produits et que nous aimerions avoir une route comme /products/3, où 3 serait l'ID du produit que l'on souhaite voir sur la page.
Ce type de route, programmative, peut facilement être défini au niveau d'un schéma de routes :
jsx
import { createBrowserRouter } from "react-router-dom";
import Home from "../components/Home.js";
import Contact from "../components/Contact.js";
import Product from "../components/Product.js";
export default createBrowserRouter([
{
path: "/",
element: <Home />,
},
{
path: "/contact",
element: <Contact />,
},
{
path: "/products/:id",
element: <Product />,
},
]);Le caractère : permet ici d'introduire un élément dynamique dans le chemin d'une route. Le nom suivant, ici id, permettra ensuite de se référer à cet élément dans l'application.
La route ajoutée redirigera toutes les URLs débutant par /products/ vers le composant Product. Le contenu du chemin situé après sera considéré comme faisant partie du paramètre :id.
Au niveau du composant Product, la valeur de l'ID pourra être récupérée pour obtenir les données du produit correspondant.
jsx
import { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
export default function Product() {
const { id } = useParams();
const [product, setProduct] = useState(null);
useEffect(() => {
const fetchData = async () => {
const response = await fetch(
`https://my-domain.com/products/${id}`
);
if (response.status === 200) {
const newProduct = await response.json();
setProduct(newProduct);
}
};
fetchData();
}, [id]);
return (
<div>
{product && (
<div>
<h2>{product.name}</h2>
<p>{product.description}</p>
</div>
)}
</div>
);
}La fonction useParams de react-router-dom permet de récupérer l'ensemble des paramètres dynamiques présents dans la route courante.
Le paramètre id est ainsi récupéré depuis la route pour effectuer une requête API et obtenir les informations du produit correspondant.
Routage par défaut
Si nous cherchons à aller sur un autre chemin que ceux définis dans le schéma de routes, l'application n'affichera aucun contenu, et l'utilisateur recevra une erreur 404.
A l'aide du caractère générique *, il est possible de définir une route qui correspondra à tous les chemins possibles sur le site :
jsx
import { createBrowserRouter, Navigate } from 'react-router-dom';
...
export default createBrowserRouter([
{
path: "/",
element: <Home />,
},
{
path: "/contact",
element: <Contact />,
},
{
path: "*",
element: <Navigate to="/" replace />,
},
]);Le composant <Navigate /> permet de rediriger une route vers une autre, ici vers la route Home.
La route avec le chemin * doit être placée à la fin du tableau. Lorsque le routeur cherche à identifier la route courante, il tente de faire correspondre l'URL avec les chemins présents dans le schéma en commençant par les routes définies en premier. Si la route générique est placée en premier, elle bloquera l'accès aux autres routes du schéma.
INFO
👉 L'URL affichée dans le navigateur sera elle aussi modifiée par la redirection. Si j'indique https://my-domain.com/something dans notre exemple, je me retrouverai immédiatement sur https://my-domain.com/.
INFO
📖 Il existe bien d'autres fonctionnalités introduites par le système de routage (middlewares, queries, sous-routes, etc.). Nous n'avons évoqué ici que les principales. Plus d'informations sont disponibles dans la documentation du plugin.
🐶 Dog Center
Nous aimerions maintenant avoir la possibilité de cliquer sur les DogCard, afin de pouvoir nous rendre sur une page dédiée au chien sélectionné.
Nous configurons donc le projet pour qu'il fonctionne avec un système de routage et définissons un schéma de routes pour l'application en utilisant une route programmative:
jsx
...
export default createBrowserRouter([
{
path: "/",
element: <Gallery />,
},
{
path: "/dogs/:id",
element: <DogPage />,
},
]);Nous renommons le composant App en composant Gallery, et le deplaçons dans le dossier src/components.
Au niveau de la galerie, nous faisons en sorte d'être redirigé si l'on clique sur la <DogCard> d'un chien à l'aide d'une balise de navigation :
jsx
import { useMemo } from "react";
import { Link } from "react-router-dom"
import DogCard from "./DogCard";
export default function GalleryContent({ dogsData, search, dogsSortBy }) {
...
return (
<div id="dog-gallery">
{filteredDogsData.map((dog) => (
<Link to={"/dogs/" + dog.id} key={dog.id}>
<DogCard
name={dog.name}
age={dog.age}
breed={dog.breed}
pictureUrl={dog.pictureUrl}
soundUrl={dog.soundUrl}
/>
</Link>
))}
</div>
);
}Attention, nos composants <DogCard> comportent un bouton qui permet d'entendre un enregistrement des chiens. Nous ne souhaitons pas être redirigé si l'utilisateur clique sur ce bouton. Il faut donc stopper la propagation de l'événement à l'aide de la méthode stopPropagation depuis la fonction exécutée par l'événement :
js
const playDogSound = (event) => {
event.stopPropagation()
audio.currentTime = 0
audio.play()
}Enfin, nous créons le composant <DogPage> vers lequel regirige le lien. Le composant récupère le paramètre id de la route pour demander à l'API les informations du chien concerné :
jsx
import { useParams } from "react-router-dom";
import { getDogDetails } from "@/services/api";
export default function DogPage() {
const [dog, setDog] = useState(null);
const { id } = useParams();
useEffect(() => {
const fetchData = async () => {
const data = await getDogDetails(id);
setDog(data);
};
fetchData();
}, [id]);
return (
<div>
{dog && (
<div>
<img src={dog.pictureUrl} alt="Dog" />
<h2>{dog.name}</h2>
{dog.age && <p>Age: {dog.age} years</p>}
<p>Breed: {dog.breed}</p>
<p>{dog.description}</p>
</div>
)}
</div>
);
}Nous passons ici par une autre requête d'API, différente de celle retournant les informations de tous les chiens. Cette nouvelle requête est elle aussi définie dans le fichier src/services/api. Elle nous permet d'obtenir les informations d'un seul chien, mais avec plus de détails.
WARNING
⚠️ Lorsque l'utilisateur change de page, les composants de la page précédente sont détruits s'ils ne sont pas présents dans la nouvelle. Les données définies dans ces composants sont alors supprimées. Il n'est donc pas possible d'utiliser la donnée dogsData depuis le composant DogPage. Cette séparation est saine. L'utilisateur peut se rendre directement sur la page d'un chien spécifique en tapant l'URL de sa page. Dans ce cas, les données des autres chiens ne seront pas inutilement chargées, seulement celles du chien présenté sur la page.
Conclusion
Le routage frontend permet la création d'applications plus élaborées, comportant plusieurs pages sur lesquelles les utilisateurs peuvent naviguer. Ce routage, entièrement géré côté frontend, offre une expérience très fluide lors du passage entre les pages, car le chargement d'une nouvelle page ne passe pas par une nouvelle requête serveur. Cela se fait au prix d'un changement plus lourd lors de l'arrivée sur l'application.
Dans le prochain chapitre, nous évoquerons les solutions permettant d'afficher des diagrammes et autres forme de data-visualisation dans une application.

