Cours JavaScript et React · Module 6: React Router
Les routes imbriquées dans la navigation intergalactique
Dans cette leçon9
Dans notre voyage cosmique à travers le monde de React Router, il est temps de plonger dans le concept des routes imbriquées. Tout comme dans l'espace réel, où nous avons des galaxies contenant des systèmes solaires, qui à leur tour contiennent des planètes avec leurs lunes - dans les applications React, nous pouvons créer des structures de navigation hiérarchiques.
Que sont les routes imbriquées ?
Les routes imbriquées sont une technique d'organisation du routage dans une application où certaines routes (composants) en contiennent d'autres (composants) en leur sein. Cela permet de créer des structures de navigation plus intuitives et hiérarchiques qui reflètent l'organisation logique de l'application.
Les routes imbriquées sont particulièrement utiles dans les grandes applications comportant de nombreuses sous-sections avec leurs propres vues.
Implémenter les routes imbriquées dans React Router
Dans React Router v6, les routes imbriquées sont implémentées en plaçant des composants Route à l'intérieur d'autres composants Route et en utilisant le composant Outlet pour indiquer où les composants des sous-routes doivent être rendus.
Structure de base des routes imbriquées
1import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom';
2
3function App() {
4 return (
5 <BrowserRouter>
6 <Routes>
7 <Route path="/" element={<Layout />}>
8 <Route index element={<Home />} />
9 <Route path="galaxies" element={<GalaxiesList />} />
10 <Route path="galaxies/:galaxyId" element={<GalaxyLayout />}>
11 <Route index element={<GalaxyOverview />} />
12 <Route path="systems" element={<SolarSystemsList />} />
13 <Route path="systems/:systemId" element={<SolarSystemDetail />} />
14 </Route>
15 </Route>
16 </Routes>
17 </BrowserRouter>
18 );
19}Dans l'exemple ci-dessus :
Layoutest le composant principal qui contiendra toutes les vues imbriquéesGalaxyLayoutest le composant qui servira de "conteneur" pour les détails de la galaxie- L'attribut
indexindique ce qui doit être rendu lorsque l'URL correspond exactement au chemin parent
Le composant Outlet
L'élément clé des routes imbriquées est le composant Outlet, qui agit comme un "placeholder" pour les composants enfants. Il détermine où, dans le composant parent, les composants des sous-routes doivent être rendus :
1import { Outlet, Link } from 'react-router-dom';
2
3function Layout() {
4 return (
5 <div className="app-layout">
6 <header>
7 <h1>Atlas cosmique</h1>
8 <nav>
9 <Link to="/">Accueil</Link>
10 <Link to="/galaxies">Galaxies</Link>
11 </nav>
12 </header>
13
14 <main>
15 {/* Les composants des sous-routes seront rendus ici */}
16 <Outlet />
17 </main>
18
19 <footer>© 2150 Centre de navigation intergalactique</footer>
20 </div>
21 );
22}
23
24function GalaxyLayout() {
25 const { galaxyId } = useParams();
26
27 return (
28 <div className="galaxy-layout">
29 <h2>Galaxie : {galaxyId}</h2>
30 <nav className="galaxy-navigation">
31 <Link to={`/galaxies/${galaxyId}`}>Vue d'ensemble</Link>
32 <Link to={`/galaxies/${galaxyId}/systems`}>Systèmes solaires</Link>
33 </nav>
34
35 <div className="galaxy-content">
36 {/* Les composants des sous-routes de la galaxie seront rendus ici */}
37 <Outlet />
38 </div>
39 </div>
40 );
41}Routes relatives et routes index
React Router v6 a introduit plusieurs fonctionnalités utiles pour mieux gérer les routes imbriquées :
1. Routes index
Une route index indique ce qui doit être rendu lorsque l'URL correspond exactement au chemin parent :
1<Route path="galaxies" element={<GalaxiesLayout />}>
2 {/* Ceci sera rendu pour /galaxies */}
3 <Route index element={<GalaxiesList />} />
4
5 {/* Ceci sera rendu pour /galaxies/:galaxyId */}
6 <Route path=":galaxyId" element={<GalaxyDetails />} />
7</Route>2. Liens relatifs
Dans les composants imbriqués, nous pouvons utiliser des liens relatifs qui sont résolus par rapport au chemin actuel :
1import { Link, useParams } from 'react-router-dom';
2
3function GalaxySystemsList() {
4 const { galaxyId } = useParams();
5
6 return (
7 <div>
8 <h3>Systèmes solaires dans la galaxie {galaxyId}</h3>
9 <ul>
10 {systems.map(system => (
11 <li key={system.id}>
12 {/* Lien relatif - sera résolu en /galaxies/:galaxyId/systems/:systemId */}
13 <Link to={system.id}>{system.name}</Link>
14 </li>
15 ))}
16 </ul>
17
18 {/* Lien relatif "vers le haut" - revenir d'un niveau */}
19 <Link to="..">Retour à la vue d'ensemble de la galaxie</Link>
20 </div>
21 );
22}Passer du contexte entre les niveaux d'imbrication
L'un des défis du travail avec les routes imbriquées est le passage de données entre les différents niveaux d'imbrication. React Router v6 offre plusieurs solutions :
1. Les hooks useOutletContext et useOutlet
Le hook useOutletContext permet de passer des données d'un composant parent à un enfant :
1import { Outlet, useOutletContext, useParams } from 'react-router-dom';
2
3function GalaxyLayout() {
4 const { galaxyId } = useParams();
5 const [galaxy, setGalaxy] = useState(null);
6
7 useEffect(() => {
8 // Récupérer les données de la galaxie
9 fetchGalaxy(galaxyId).then(data => setGalaxy(data));
10 }, [galaxyId]);
11
12 // Passer les données de la galaxie aux composants enfants
13 return (
14 <div className="galaxy-layout">
15 <h2>{galaxy?.name || 'Chargement...'}</h2>
16 <Outlet context={{ galaxy, updateGalaxy: setGalaxy }} />
17 </div>
18 );
19}
20
21function GalaxyOverview() {
22 // Recevoir le contexte du composant parent
23 const { galaxy, updateGalaxy } = useOutletContext();
24
25 if (!galaxy) return <div>Chargement des données de la galaxie...</div>;
26
27 return (
28 <div className="galaxy-overview">
29 <h3>Vue d'ensemble de la galaxie {galaxy.name}</h3>
30 <p>Type : {galaxy.type}</p>
31 <p>Nombre de systèmes : {galaxy.systemsCount}</p>
32 {/* Reste du composant */}
33 </div>
34 );
35}2. État partagé avec React Context
Pour des scénarios plus complexes, React Context peut être utilisé pour partager l'état entre les composants :
1import { createContext, useContext, useState } from 'react';
2import { Outlet } from 'react-router-dom';
3
4// Créer le contexte
5const GalaxyContext = createContext();
6
7// Hook pour accéder facilement au contexte
8function useGalaxy() {
9 return useContext(GalaxyContext);
10}
11
12function GalaxyProvider({ children }) {
13 const [selectedGalaxy, setSelectedGalaxy] = useState(null);
14 const [favoritesSystems, setFavoriteSystems] = useState([]);
15
16 const addFavoriteSystem = (system) => {
17 setFavoriteSystems(prev => [...prev, system]);
18 };
19
20 // Valeur du contexte
21 const value = {
22 selectedGalaxy,
23 setSelectedGalaxy,
24 favoritesSystems,
25 addFavoriteSystem
26 };
27
28 return (
29 <GalaxyContext.Provider value={value}>
30 {children}
31 </GalaxyContext.Provider>
32 );
33}
34
35function GalaxiesRoot() {
36 return (
37 <GalaxyProvider>
38 <div className="galaxies-section">
39 <h2>Exploration des galaxies</h2>
40 <Outlet />
41 </div>
42 </GalaxyProvider>
43 );
44}
45
46// Utilisation dans un composant enfant
47function SolarSystemDetail() {
48 const { favoritesSystems, addFavoriteSystem } = useGalaxy();
49 const { systemId } = useParams();
50
51 // Reste du composant
52}Modèles d'imbrication avancés
Les routes imbriquées peuvent être utilisées pour implémenter divers modèles de navigation :
1. Mise en page multi-niveaux
1<Routes>
2 <Route path="/" element={<MainLayout />}>
3 <Route element={<AuthLayout />}>
4 <Route path="dashboard" element={<Dashboard />} />
5 <Route path="profile" element={<Profile />} />
6 <Route path="settings" element={<SettingsLayout />}>
7 <Route path="account" element={<AccountSettings />} />
8 <Route path="notifications" element={<NotificationSettings />} />
9 </Route>
10 </Route>
11 <Route path="about" element={<About />} />
12 <Route path="contact" element={<Contact />} />
13 </Route>
14</Routes>Dans cet exemple :
MainLayoutcontient la structure générale de la pageAuthLayoutgère l'authentification pour les chemins protégésSettingsLayoutcrée un niveau de navigation supplémentaire pour les paramètres
2. Segments dynamiques et imbrication
1<Routes>
2 <Route path="galaxies" element={<GalaxiesList />} />
3 <Route path="galaxies/:galaxyId" element={<GalaxyLayout />}>
4 <Route index element={<GalaxyOverview />} />
5 <Route path="systems" element={<SystemsList />} />
6 <Route path="systems/:systemId" element={<SystemLayout />}>
7 <Route index element={<SystemOverview />} />
8 <Route path="planets" element={<PlanetsList />} />
9 <Route path="planets/:planetId" element={<PlanetDetails />} />
10 </Route>
11 </Route>
12</Routes>Cette structure crée une imbrication profonde de routes qui reflète la hiérarchie galaxie > système solaire > planète.
3. Plusieurs emplacements dans un seul layout
Le tableau de bord de la galaxie comporte trois zones : la navigation à gauche, la vue principale au centre et la télémétrie à droite. Si tu connais les router-outlet nommés d'Angular ou les vues nommées de Vue Router, tu pourrais chercher quelque chose de semblable dans React Router. Il n'y a pas d'outlets nommés ici : Outlet n'accepte qu'une seule prop, context, et rend le composant de la sous-route correspondante (ou rien si aucune ne correspond). Si tu écrivais <Outlet name="sidebar" />, React Router ignorerait la prop inconnue et rendrait à cet endroit la même sous-route que l'Outlet principal.
Au lieu d'emplacements nommés, le layout a un seul Outlet pour la vue principale, et des composants ordinaires remplissent les autres zones :
1import { Outlet } from 'react-router-dom';
2
3function DashboardLayout() {
4 return (
5 <div className="dashboard">
6 <aside className="sidebar">
7 {/* Un composant ordinaire de navigation latérale */}
8 <MissionSidebar />
9 </aside>
10 <div className="main-content">
11 {/* Le seul Outlet : la sous-route correspondante s'affiche ici */}
12 <Outlet />
13 </div>
14 <aside className="auxiliary-panel">
15 {/* Un composant ordinaire de panneau auxiliaire */}
16 <AuxiliaryPanel />
17 </aside>
18 </div>
19 );
20}La disposition en trois zones n'a pas changé, seul ce qui les remplit a changé. Le centre dépend de l'adresse, tandis que les panneaux latéraux s'affichent toujours et décident eux-mêmes de ce qu'ils montrent.
Supposons que DashboardLayout soit l'élément de la route galaxies/:galaxyId des exemples précédents. Un panneau latéral peut alors lire l'adresse avec des hooks : useParams, que tu connais déjà, et useLocation, qui renvoie un objet décrivant l'adresse actuelle, notamment le chemin dans son champ pathname. Tu en apprendras davantage sur useLocation dans les prochaines leçons.
1import { useLocation, useParams } from 'react-router-dom';
2
3function AuxiliaryPanel() {
4 const { galaxyId } = useParams();
5 const { pathname } = useLocation();
6
7 return (
8 <div>
9 <h3>Télémétrie de la galaxie {galaxyId}</h3>
10 <p>Vue actuelle : {pathname}</p>
11 </div>
12 );
13}À l'adresse /galaxies/andromeda/systems, le panneau affiche un titre avec l'identifiant andromeda et le chemin complet. Le panneau lui-même ne connaît pas les sous-routes : il lit seulement les paramètres de sa propre route et l'adresse.
Parfois, c'est la sous-route qui doit décider de ce qui apparaît dans le panneau. C'est le rôle de la propriété de route handle : des données quelconques qu'une route attache à sa correspondance. Tu les lis avec le hook useMatches(), qui renvoie toutes les correspondances actives, du layout jusqu'à la sous-route la plus profonde. La documentation de React Router présente ce motif pour le fil d'Ariane (breadcrumbs). Une condition toutefois : useMatches ne fonctionne que dans un routeur de données (data router), c'est-à-dire quand tu passes tes routes à createBrowserRouter et que tu rends l'application avec <RouterProvider router={router} />. Un tel routeur connaît tout l'arbre des routes à l'avance. Dans <BrowserRouter>, le hook lève une erreur.
1import { createBrowserRouter } from 'react-router-dom';
2
3const router = createBrowserRouter([
4 {
5 path: '/galaxies/:galaxyId',
6 element: <DashboardLayout />,
7 children: [
8 { index: true, element: <GalaxyOverview /> },
9 {
10 path: 'systems',
11 element: <SystemsList />,
12 // handle : des données quelconques attachées à la correspondance de cette route
13 handle: { sidebar: () => <SystemsFilter /> },
14 },
15 ],
16 },
17]);Ici, les routes sont décrites par un tableau d'objets au lieu de composants <Route>, mais la structure reste la même : un layout avec ses sous-routes dans children, et index: true joue le rôle de l'attribut index. La seule nouveauté est la propriété handle de la route systems : son champ sidebar est une fonction qui renvoie le panneau. À présent, MissionSidebar parcourt les correspondances en partant de la plus profonde et utilise le premier handle.sidebar trouvé :
1import { useMatches } from 'react-router-dom';
2
3function MissionSidebar() {
4 const matches = useMatches();
5 const match = [...matches].reverse().find((m) => m.handle?.sidebar);
6
7 return match ? match.handle.sidebar() : <p>Choisis un système solaire</p>;
8}La copie du tableau ([...matches]) est nécessaire, car reverse() inverse un tableau sur place. Sur /galaxies/andromeda/systems, le panneau de gauche affiche SystemsFilter, parce que la route systems l'a décidé. Sur /galaxies/andromeda, la route index n'a pas de handle, donc le panneau affiche le texte par défaut, et le layout garde tout du long exactement un seul Outlet. Je te conseille de commencer par des composants ordinaires avec useParams et useLocation, et de ne recourir à handle que lorsque chaque sous-route doit avoir son propre contenu dans le panneau. Tu retrouveras le routeur de données lors du blocage de la navigation, dans la leçon sur l'historique du navigateur.
Optimiser les routes imbriquées
Lors du travail avec des routes imbriquées, il vaut la peine de garder en tête quelques techniques d'optimisation :
1. Chargement différé des composants
Pour améliorer les performances, les composants peuvent être chargés dynamiquement uniquement lorsqu'ils sont nécessaires :
1import { Suspense, lazy } from 'react';
2import { Routes, Route } from 'react-router-dom';
3
4// Chargement différé des composants
5const GalaxyLayout = lazy(() => import('./GalaxyLayout'));
6const GalaxiesList = lazy(() => import('./GalaxiesList'));
7const SolarSystemDetails = lazy(() => import('./SolarSystemDetails'));
8
9function App() {
10 return (
11 <Routes>
12 <Route path="/" element={<MainLayout />}>
13 {/* Composant chargé de façon différée */}
14 <Route
15 path="galaxies"
16 element={
17 <Suspense fallback={<div>Chargement des galaxies...</div>}>
18 <GalaxiesList />
19 </Suspense>
20 }
21 />
22 <Route
23 path="galaxies/:galaxyId/*"
24 element={
25 <Suspense fallback={<div>Chargement des données de la galaxie...</div>}>
26 <GalaxyLayout />
27 </Suspense>
28 }
29 />
30 </Route>
31 </Routes>
32 );
33}2. Optimisation du rendu
L'optimisation du rendu peut être particulièrement importante avec une imbrication profonde, lorsqu'un changement de paramètre d'URL à un niveau profond peut provoquer un re-rendu inutile des composants de niveau supérieur :
1import { memo } from 'react';
2import { Outlet, useParams } from 'react-router-dom';
3
4// Composant optimisé - ne se re-rendra pas si ses props ne changent pas
5const MemoizedGalaxyLayout = memo(function GalaxyLayout() {
6 const { galaxyId } = useParams();
7
8 console.log(`Rendu de GalaxyLayout pour la galaxie ${galaxyId}`);
9
10 return (
11 <div className="galaxy-layout">
12 <h2>Galaxie : {galaxyId}</h2>
13 <Outlet />
14 </div>
15 );
16});Exemple complet d'application avec routes imbriquées
Voici un exemple d'application spatiale complète avec des routes imbriquées :
1import React from 'react';
2import {
3 BrowserRouter,
4 Routes,
5 Route,
6 Outlet,
7 Link,
8 NavLink,
9 useParams,
10 useOutletContext,
11 useNavigate
12} from 'react-router-dom';
13
14// Mise en page principale de l'application
15function AppLayout() {
16 return (
17 <div className="cosmos-explorer">
18 <header className="main-header">
19 <h1>Atlas du voyageur cosmique</h1>
20 <MainNavigation />
21 </header>
22
23 <main className="main-content">
24 <Outlet />
25 </main>
26
27 <footer className="main-footer">
28 <p>© 2150 Agence de voyages intergalactiques</p>
29 </footer>
30 </div>
31 );
32}
33
34// Navigation principale
35function MainNavigation() {
36 return (
37 <nav className="main-nav">
38 <ul>
39 <li><NavLink to="/" end>Centre de commandement</NavLink></li>
40 <li><NavLink to="/galaxies">Galaxies</NavLink></li>
41 <li><NavLink to="/missions">Missions</NavLink></li>
42 <li><NavLink to="/crew">Équipage</NavLink></li>
43 </ul>
44 </nav>
45 );
46}
47
48// Page d'accueil
49function Dashboard() {
50 return (
51 <div className="dashboard">
52 <h2>Centre de commandement du vaisseau</h2>
53 <p>Bienvenue, Capitaine ! Quel emplacement souhaiterais-tu explorer aujourd'hui ?</p>
54
55 <div className="dashboard-tiles">
56 <div className="tile">
57 <h3>Galaxies</h3>
58 <p>Explore différentes galaxies de l'univers</p>
59 <Link to="/galaxies" className="tile-link">Aller aux galaxies</Link>
60 </div>
61
62 <div className="tile">
63 <h3>Missions</h3>
64 <p>Parcours les missions spatiales actuelles et futures</p>
65 <Link to="/missions" className="tile-link">Aller aux missions</Link>
66 </div>
67
68 <div className="tile">
69 <h3>Équipage</h3>
70 <p>Gère les membres de l'équipage du vaisseau</p>
71 <Link to="/crew" className="tile-link">Aller à l'équipage</Link>
72 </div>
73 </div>
74 </div>
75 );
76}
77
78// ===== SECTION GALAXIES =====
79
80// Mise en page des galaxies
81function GalaxiesLayout() {
82 return (
83 <div className="galaxies-section">
84 <h2>Exploration des galaxies</h2>
85 <Outlet />
86 </div>
87 );
88}
89
90// Liste des galaxies
91function GalaxiesList() {
92 const galaxies = [
93 { id: 'milky-way', name: 'Voie lactée', type: 'Spirale' },
94 { id: 'andromeda', name: 'Andromède', type: 'Spirale' },
95 { id: 'triangulum', name: 'Galaxie du Triangle', type: 'Spirale' },
96 { id: 'large-magellanic', name: 'Grand Nuage de Magellan', type: 'Irrégulière' }
97 ];
98
99 return (
100 <div className="galaxies-list">
101 <h3>Galaxies disponibles</h3>
102 <div className="galaxy-cards">
103 {galaxies.map(galaxy => (
104 <div key={galaxy.id} className="galaxy-card">
105 <h4>{galaxy.name}</h4>
106 <p>Type : {galaxy.type}</p>
107 <Link to={galaxy.id} className="galaxy-link">
108 Explorer la galaxie
109 </Link>
110 </div>
111 ))}
112 </div>
113 </div>
114 );
115}
116
117// Mise en page des détails de la galaxie
118function GalaxyLayout() {
119 const { galaxyId } = useParams();
120 const navigate = useNavigate();
121
122 // Simulation de données de galaxie (récupérées depuis une API dans une vraie application)
123 const galaxyData = {
124 'milky-way': {
125 name: 'Voie lactée',
126 type: 'Spirale',
127 diameter: '100 000 années-lumière',
128 stars: '200-400 milliards',
129 systems: [
130 { id: 'solar', name: 'Système solaire' },
131 { id: 'alpha-centauri', name: 'Alpha Centauri' },
132 { id: 'sirius', name: 'Sirius' }
133 ]
134 },
135 'andromeda': {
136 name: 'Andromède',
137 type: 'Spirale',
138 diameter: '220 000 années-lumière',
139 stars: '1 billion',
140 systems: [
141 { id: 'system-1', name: 'Système M31-A' },
142 { id: 'system-2', name: 'Système M31-B' }
143 ]
144 },
145 'triangulum': {
146 name: 'Galaxie du Triangle',
147 type: 'Spirale',
148 diameter: '60 000 années-lumière',
149 stars: '40 milliards',
150 systems: [
151 { id: 'system-1', name: 'Système T1' }
152 ]
153 },
154 'large-magellanic': {
155 name: 'Grand Nuage de Magellan',
156 type: 'Irrégulière',
157 diameter: '14 000 années-lumière',
158 stars: '30 milliards',
159 systems: []
160 }
161 };
162
163 const galaxy = galaxyData[galaxyId];
164
165 if (!galaxy) {
166 return (
167 <div className="galaxy-not-found">
168 <h3>Galaxie introuvable</h3>
169 <p>Aucune galaxie trouvée avec l'identifiant : {galaxyId}</p>
170 <button onClick={() => navigate('/galaxies')}>
171 Retour à la liste des galaxies
172 </button>
173 </div>
174 );
175 }
176
177 return (
178 <div className="galaxy-details">
179 <h3>Galaxie : {galaxy.name}</h3>
180
181 <nav className="galaxy-nav">
182 <NavLink to="" end>Vue d'ensemble</NavLink>
183 <NavLink to="systems">Systèmes solaires</NavLink>
184 <NavLink to="map">Carte de la galaxie</NavLink>
185 </nav>
186
187 <div className="galaxy-content">
188 <Outlet context={{ galaxy }} />
189 </div>
190
191 <div className="back-navigation">
192 <Link to="/galaxies">← Retour à la liste des galaxies</Link>
193 </div>
194 </div>
195 );
196}
197
198// Vue d'ensemble de la galaxie (vue par défaut)
199function GalaxyOverview() {
200 const { galaxy } = useOutletContext();
201
202 return (
203 <div className="galaxy-overview">
204 <h4>Vue d'ensemble : {galaxy.name}</h4>
205 <table className="galaxy-stats">
206 <tbody>
207 <tr>
208 <th>Type :</th>
209 <td>{galaxy.type}</td>
210 </tr>
211 <tr>
212 <th>Diamètre :</th>
213 <td>{galaxy.diameter}</td>
214 </tr>
215 <tr>
216 <th>Nombre d'étoiles :</th>
217 <td>{galaxy.stars}</td>
218 </tr>
219 <tr>
220 <th>Systèmes découverts :</th>
221 <td>{galaxy.systems.length}</td>
222 </tr>
223 </tbody>
224 </table>
225
226 <h5>Le saviez-vous ?</h5>
227 <p>Des anecdotes uniques sur la galaxie {galaxy.name} apparaîtront ici...</p>
228 </div>
229 );
230}
231
232// Liste des systèmes solaires de la galaxie
233function GalaxySystems() {
234 const { galaxy } = useOutletContext();
235 const { galaxyId } = useParams();
236
237 return (
238 <div className="galaxy-systems">
239 <h4>Systèmes solaires dans {galaxy.name}</h4>
240
241 {galaxy.systems.length > 0 ? (
242 <ul className="systems-list">
243 {galaxy.systems.map(system => (
244 <li key={system.id} className="system-item">
245 <div className="system-info">
246 <h5>{system.name}</h5>
247 </div>
248 <Link to={`/galaxies/${galaxyId}/systems/${system.id}`}>
249 Explorer le système
250 </Link>
251 </li>
252 ))}
253 </ul>
254 ) : (
255 <p>Aucun système n'a encore été découvert dans cette galaxie.</p>
256 )}
257 </div>
258 );
259}
260
261// Carte de la galaxie
262function GalaxyMap() {
263 const { galaxy } = useOutletContext();
264
265 return (
266 <div className="galaxy-map">
267 <h4>Carte de la galaxie : {galaxy.name}</h4>
268 <div className="map-placeholder">
269 <p>Une carte interactive de la galaxie {galaxy.name} apparaîtra ici.</p>
270 <p>Actuellement en développement...</p>
271 </div>
272 </div>
273 );
274}
275
276// ===== SECTION SYSTÈMES SOLAIRES =====
277
278// Détails du système solaire
279function SolarSystemDetails() {
280 const { galaxyId, systemId } = useParams();
281 const navigate = useNavigate();
282
283 // Dans une vraie application, les données seraient récupérées en fonction de galaxyId et systemId
284 const systemData = {
285 'solar': {
286 name: 'Système solaire',
287 star: 'Soleil',
288 planets: 8,
289 age: '4,6 milliards d\'années'
290 },
291 'alpha-centauri': {
292 name: 'Alpha Centauri',
293 star: 'Alpha Centauri A et B, Proxima Centauri',
294 planets: 3,
295 age: '4,85 milliards d\'années'
296 },
297 'sirius': {
298 name: 'Sirius',
299 star: 'Sirius A et B',
300 planets: 0,
301 age: '230 millions d\'années'
302 },
303 'system-1': {
304 name: 'Système M31-A',
305 star: 'Étoile M31-A',
306 planets: 5,
307 age: '2,3 milliards d\'années'
308 },
309 'system-2': {
310 name: 'Système M31-B',
311 star: 'Étoile M31-B',
312 planets: 7,
313 age: '3,1 milliards d\'années'
314 }
315 };
316
317 const system = systemData[systemId];
318
319 if (!system) {
320 return (
321 <div className="system-not-found">
322 <h4>Système introuvable</h4>
323 <p>Aucun système solaire trouvé avec l'identifiant : {systemId}</p>
324 <button onClick={() => navigate(`/galaxies/${galaxyId}/systems`)}>
325 Retour à la liste des systèmes
326 </button>
327 </div>
328 );
329 }
330
331 return (
332 <div className="system-details">
333 <h4>Système solaire : {system.name}</h4>
334
335 <table className="system-stats">
336 <tbody>
337 <tr>
338 <th>Étoile(s) :</th>
339 <td>{system.star}</td>
340 </tr>
341 <tr>
342 <th>Nombre de planètes :</th>
343 <td>{system.planets}</td>
344 </tr>
345 <tr>
346 <th>Âge :</th>
347 <td>{system.age}</td>
348 </tr>
349 </tbody>
350 </table>
351
352 <div className="back-navigation">
353 <Link to={`/galaxies/${galaxyId}/systems`}>
354 ← Retour aux systèmes solaires
355 </Link>
356 </div>
357 </div>
358 );
359}
360
361// ===== SECTION MISSIONS =====
362
363// Mise en page des missions
364function MissionsLayout() {
365 return (
366 <div className="missions-section">
367 <h2>Missions spatiales</h2>
368 <Outlet />
369 </div>
370 );
371}
372
373// Liste des missions
374function MissionsList() {
375 const missions = [
376 { id: 'apollo', name: 'Apollo', status: 'Terminée' },
377 { id: 'artemis', name: 'Artemis', status: 'En cours' },
378 { id: 'mars-2030', name: 'Mars 2030', status: 'Planifiée' }
379 ];
380
381 return (
382 <div className="missions-list">
383 <h3>Missions spatiales</h3>
384 <ul>
385 {missions.map(mission => (
386 <li key={mission.id}>
387 <Link to={mission.id}>{mission.name}</Link> - {mission.status}
388 </li>
389 ))}
390 </ul>
391 </div>
392 );
393}
394
395// Détails de la mission
396function MissionDetails() {
397 const { missionId } = useParams();
398
399 return (
400 <div className="mission-details">
401 <h3>Détails de la mission : {missionId}</h3>
402 <p>Les détails de la mission {missionId} apparaîtront ici...</p>
403 <Link to="/missions">← Retour à la liste des missions</Link>
404 </div>
405 );
406}
407
408// ===== SECTION ÉQUIPAGE =====
409
410// Mise en page de l'équipage
411function CrewLayout() {
412 return (
413 <div className="crew-section">
414 <h2>Gestion de l'équipage</h2>
415 <Outlet />
416 </div>
417 );
418}
419
420// Liste des membres de l'équipage
421function CrewList() {
422 const crewMembers = [
423 { id: 'crew1', name: 'John Smith', role: 'Capitaine' },
424 { id: 'crew2', name: 'Anna Johnson', role: 'Navigatrice' },
425 { id: 'crew3', name: 'Peter Williams', role: 'Ingénieur' }
426 ];
427
428 return (
429 <div className="crew-list">
430 <h3>Membres de l'équipage</h3>
431 <ul>
432 {crewMembers.map(member => (
433 <li key={member.id}>
434 <Link to={member.id}>{member.name}</Link> - {member.role}
435 </li>
436 ))}
437 </ul>
438 </div>
439 );
440}
441
442// Détails d'un membre de l'équipage
443function CrewMemberDetails() {
444 const { crewId } = useParams();
445
446 return (
447 <div className="crew-member-details">
448 <h3>Profil du membre de l'équipage : {crewId}</h3>
449 <p>Le profil du membre de l'équipage {crewId} apparaîtra ici...</p>
450 <Link to="/crew">← Retour à la liste de l'équipage</Link>
451 </div>
452 );
453}
454
455// ===== COMPOSANT PRINCIPAL DE L'APPLICATION =====
456
457function App() {
458 return (
459 <BrowserRouter>
460 <Routes>
461 <Route path="/" element={<AppLayout />}>
462 {/* Page d'accueil */}
463 <Route index element={<Dashboard />} />
464
465 {/* Section galaxies */}
466 <Route path="galaxies" element={<GalaxiesLayout />}>
467 <Route index element={<GalaxiesList />} />
468 <Route path=":galaxyId" element={<GalaxyLayout />}>
469 <Route index element={<GalaxyOverview />} />
470 <Route path="systems" element={<GalaxySystems />} />
471 <Route path="map" element={<GalaxyMap />} />
472 </Route>
473 <Route path=":galaxyId/systems/:systemId" element={<SolarSystemDetails />} />
474 </Route>
475
476 {/* Section missions */}
477 <Route path="missions" element={<MissionsLayout />}>
478 <Route index element={<MissionsList />} />
479 <Route path=":missionId" element={<MissionDetails />} />
480 </Route>
481
482 {/* Section équipage */}
483 <Route path="crew" element={<CrewLayout />}>
484 <Route index element={<CrewList />} />
485 <Route path=":crewId" element={<CrewMemberDetails />} />
486 </Route>
487
488 {/* Gestion du 404 */}
489 <Route path="*" element={<NotFound />} />
490 </Route>
491 </Routes>
492 </BrowserRouter>
493 );
494}
495
496// Composant pour les chemins inexistants
497function NotFound() {
498 return (
499 <div className="not-found">
500 <h2>404 - Perdu dans l'espace</h2>
501 <p>Ce chemin ne mène à aucune destination connue dans l'espace.</p>
502 <Link to="/">Retour au centre de commandement</Link>
503 </div>
504 );
505}
506
507export default App;Bonnes pratiques pour les routes imbriquées
- Maintenir une hiérarchie logique - La structure des routes imbriquées doit refléter l'organisation logique de l'application
- Utiliser des composants de mise en page - Crée des composants de mise en page dédiés pour chaque niveau d'imbrication
- Optimiser le passage de données - Utilise useOutletContext ou React Context pour un passage de données efficace
- Assurer une navigation intuitive - Ajoute toujours des liens permettant de naviguer facilement vers le haut et le bas de la hiérarchie
- Diviser le code - Utilise le chargement différé pour optimiser les performances, surtout pour les composants profondément imbriqués
- Garder des URL propres - Conçois des URL lisibles et logiques pour l'utilisateur
- Éviter une imbrication trop profonde - Essaie de ne pas dépasser 3-4 niveaux d'imbrication pour la clarté
Résumé
Les routes imbriquées dans React Router sont une technique puissante pour organiser la navigation dans les applications React, permettant de créer des structures d'interface utilisateur intuitives et hiérarchiques. Avantages clés :
- Organisation logique - La structure de routage reflète la hiérarchie logique de l'application
- Réutilisation des mises en page - Les éléments d'interface communs peuvent être partagés entre les sous-routes
- Meilleure UX - Les utilisateurs peuvent facilement s'orienter dans la structure de l'application
- Gestion d'état plus facile - L'état peut être stocké au niveau d'imbrication approprié
Tout comme dans l'exploration spatiale réelle, où nous voyageons d'une galaxie, à travers les systèmes solaires, jusqu'aux planètes et aux lunes - les routes imbriquées permettent de créer des systèmes de navigation intuitifs qui guident l'utilisateur à travers les différents niveaux de notre application spatiale.
Code de cette leçon : App.jsx
1import React, { useState } from 'react';
2import { BrowserRouter, Routes, Route, Link, useNavigate, useLocation } from 'react-router-dom';
3
4// Page de connexion
5function LoginPage() {
6 const [username, setUsername] = useState('');
7 const navigate = useNavigate();
8
9 const handleLogin = (e) => {
10 e.preventDefault();
11 if (username.trim()) {
12 // Navigation programmatique avec passage de state
13 navigate('/dashboard', { state: { username } });
14 }
15 };
16
17 return (
18 <div className="page login">
19 <h1>Panneau de connexion</h1>
20 <form onSubmit={handleLogin}>
21 <input
22 type="text"
23 value={username}
24 onChange={(e) => setUsername(e.target.value)}
25 placeholder="Saisis ton nom d'utilisateur"
26 />
27 <button type="submit">Se connecter</button>
28 </form>
29 </div>
30 );
31}
32
33// Dashboard - en utilisant useLocation pour lire le state
34function Dashboard() {
35 const location = useLocation();
36 const navigate = useNavigate();
37 const username = location.state?.username || 'Invité';
38
39 const handleLogout = () => {
40 // Navigation avec replace: true - n'ajoute pas à l'historique
41 navigate('/', { replace: true });
42 };
43
44 const goToMission = (missionId) => {
45 navigate(`/mission/${missionId}`);
46 };
47
48 return (
49 <div className="page dashboard">
50 <h1>Bienvenue, {username} !</h1>
51 <p>Ton panneau de contrôle de mission spatiale</p>
52
53 <div className="missions">
54 <h2>Missions disponibles :</h2>
55 <div className="mission-buttons">
56 <button onClick={() => goToMission('apollo')}>Apollo</button>
57 <button onClick={() => goToMission('artemis')}>Artemis</button>
58 <button onClick={() => goToMission('voyager')}>Voyager</button>
59 </div>
60 </div>
61
62 <div className="navigation-demo">
63 <h2>Navigation programmatique :</h2>
64 <button onClick={() => navigate(-1)}>← Retour (go -1)</button>
65 <button onClick={() => navigate(1)}>Suivant (go 1) →</button>
66 <button onClick={handleLogout} className="logout">Déconnexion</button>
67 </div>
68 </div>
69 );
70}
71
72// Page de mission
73function MissionPage() {
74 const navigate = useNavigate();
75 const location = useLocation();
76
77 // Récupérer l'ID de la mission depuis l'URL
78 const missionId = location.pathname.split('/').pop();
79
80 return (
81 <div className="page mission">
82 <h1>Mission : {missionId.toUpperCase()}</h1>
83 <p>Détails de la mission {missionId}</p>
84
85 <div className="location-info">
86 <h3>Informations sur l'emplacement :</h3>
87 <pre>{JSON.stringify(location, null, 2)}</pre>
88 </div>
89
90 <button onClick={() => navigate('/dashboard', { state: location.state })}>
91 ← Retour au panneau
92 </button>
93 </div>
94 );
95}
96
97function App() {
98 return (
99 <BrowserRouter>
100 <div className="app">
101 <nav>
102 <Link to="/">Connexion</Link>
103 <Link to="/dashboard">Dashboard</Link>
104 </nav>
105 <Routes>
106 <Route path="/" element={<LoginPage />} />
107 <Route path="/dashboard" element={<Dashboard />} />
108 <Route path="/mission/:id" element={<MissionPage />} />
109 </Routes>
110 </div>
111 </BrowserRouter>
112 );
113}
114
115export default App;Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Le hook useParams permet de :
2. Le hook useSearchParams sert à :
Tâches pratiques dans le jeu
- Éditeur de code
Routes imbriquées avec Outlet
- Tri horizontal
Ordonne les éléments de la destructuration de useParams, de gauche à droite :
- Éditeur de code
Paramètres de recherche et filtrage
- Tri vertical
Ordonne les étapes de configuration des routes imbriquées :