JavaScript and React course Β· Module 6: React Router
Nested Routes in Intergalactic Navigation
In this lesson9
On our cosmic journey through the world of React Router, it's time to delve into the concept of nested routes. Just as in real outer space, where we have galaxies containing solar systems, which in turn contain planets with their moons - in React applications we can create hierarchical navigation structures.
What are Nested Routes?
Nested routes are a technique for organizing routing in an application where some routes (components) contain other routes (components) within themselves. This allows for creating more intuitive, hierarchical navigation structures that reflect the logical layout of the application.
Nested routes are particularly useful in larger applications where there are many subsections with their own views.
Implementing Nested Routes in React Router
In React Router v6, nested routes are implemented by placing Route components inside other Route components and using the Outlet component to specify where sub-route components should be rendered.
Basic Structure of Nested Routes
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}In the example above:
Layoutis the main component that will contain all nested viewsGalaxyLayoutis the component that will serve as a "container" for galaxy details- The
indexattribute specifies what should be rendered when the URL matches exactly the parent path
The Outlet Component
The key element of nested routes is the Outlet component, which acts as a "placeholder" for child components. It specifies where in the parent component the child route components should be rendered:
1import { Outlet, Link } from 'react-router-dom';
2
3function Layout() {
4 return (
5 <div className="app-layout">
6 <header>
7 <h1>Cosmic Atlas</h1>
8 <nav>
9 <Link to="/">Home</Link>
10 <Link to="/galaxies">Galaxies</Link>
11 </nav>
12 </header>
13
14 <main>
15 {/* Sub-route components will be rendered here */}
16 <Outlet />
17 </main>
18
19 <footer>© 2150 Intergalactic Navigation Center</footer>
20 </div>
21 );
22}
23
24function GalaxyLayout() {
25 const { galaxyId } = useParams();
26
27 return (
28 <div className="galaxy-layout">
29 <h2>Galaxy: {galaxyId}</h2>
30 <nav className="galaxy-navigation">
31 <Link to={`/galaxies/${galaxyId}`}>Overview</Link>
32 <Link to={`/galaxies/${galaxyId}/systems`}>Solar Systems</Link>
33 </nav>
34
35 <div className="galaxy-content">
36 {/* Galaxy sub-route components will be rendered here */}
37 <Outlet />
38 </div>
39 </div>
40 );
41}Relative and Index Routes
React Router v6 introduced several useful features for better handling of nested routes:
1. Index Routes
An index route specifies what should be rendered when the URL matches exactly the parent path:
1<Route path="galaxies" element={<GalaxiesLayout />}>
2 {/* This will be rendered for /galaxies */}
3 <Route index element={<GalaxiesList />} />
4
5 {/* This will be rendered for /galaxies/:galaxyId */}
6 <Route path=":galaxyId" element={<GalaxyDetails />} />
7</Route>2. Relative Links
In nested components, we can use relative links that are resolved relative to the current path:
1import { Link, useParams } from 'react-router-dom';
2
3function GalaxySystemsList() {
4 const { galaxyId } = useParams();
5
6 return (
7 <div>
8 <h3>Solar systems in galaxy {galaxyId}</h3>
9 <ul>
10 {systems.map(system => (
11 <li key={system.id}>
12 {/* Relative link - will be resolved to /galaxies/:galaxyId/systems/:systemId */}
13 <Link to={system.id}>{system.name}</Link>
14 </li>
15 ))}
16 </ul>
17
18 {/* Relative link "up" - go back one level */}
19 <Link to="..">Return to galaxy overview</Link>
20 </div>
21 );
22}Passing Context Between Nesting Levels
One of the challenges when working with nested routes is passing data between different nesting levels. React Router v6 offers several solutions:
1. useOutletContext and useOutlet Hooks
The useOutletContext hook allows passing data from a parent component to a child:
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 // Fetch galaxy data
9 fetchGalaxy(galaxyId).then(data => setGalaxy(data));
10 }, [galaxyId]);
11
12 // Pass galaxy data to child components
13 return (
14 <div className="galaxy-layout">
15 <h2>{galaxy?.name || 'Loading...'}</h2>
16 <Outlet context={{ galaxy, updateGalaxy: setGalaxy }} />
17 </div>
18 );
19}
20
21function GalaxyOverview() {
22 // Receive context from the parent component
23 const { galaxy, updateGalaxy } = useOutletContext();
24
25 if (!galaxy) return <div>Loading galaxy data...</div>;
26
27 return (
28 <div className="galaxy-overview">
29 <h3>Overview of galaxy {galaxy.name}</h3>
30 <p>Type: {galaxy.type}</p>
31 <p>Number of systems: {galaxy.systemsCount}</p>
32 {/* Rest of the component */}
33 </div>
34 );
35}2. Shared State with React Context
For more complex scenarios, React Context can be used to share state between components:
1import { createContext, useContext, useState } from 'react';
2import { Outlet } from 'react-router-dom';
3
4// Create context
5const GalaxyContext = createContext();
6
7// Hook for easy context access
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 // Context value
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>Galaxy Exploration</h2>
40 <Outlet />
41 </div>
42 </GalaxyProvider>
43 );
44}
45
46// Usage in a child component
47function SolarSystemDetail() {
48 const { favoritesSystems, addFavoriteSystem } = useGalaxy();
49 const { systemId } = useParams();
50
51 // Rest of the component
52}Advanced Nesting Patterns
Nested routes can be used to implement various navigation patterns:
1. Multi-level Layout
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>In this example:
MainLayoutcontains the general page structureAuthLayouthandles authentication for protected pathsSettingsLayoutcreates an additional navigation level for settings
2. Dynamic Segments and Nesting
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>This structure creates deep nesting of routes that reflects the hierarchy galaxy > solar system > planet.
3. Multiple Slots in One Layout
The galaxy control panel has three zones: navigation on the left, the main view in the middle and telemetry on the right. If you know Angular's named router-outlet or Vue Router's named views, you might look for something similar in React Router. There are no named outlets here: Outlet accepts only one prop, context, and renders the component of the matching child route (or nothing if no child route matches). If you wrote <Outlet name="sidebar" />, React Router would ignore the unknown prop and render the same child route in that spot as the main Outlet.
Instead of named slots, the layout has one Outlet for the main view, and regular components fill the other zones:
1import { Outlet } from 'react-router-dom';
2
3function DashboardLayout() {
4 return (
5 <div className="dashboard">
6 <aside className="sidebar">
7 {/* A regular sidebar navigation component */}
8 <MissionSidebar />
9 </aside>
10 <div className="main-content">
11 {/* The only Outlet: the matching child route goes here */}
12 <Outlet />
13 </div>
14 <aside className="auxiliary-panel">
15 {/* A regular auxiliary panel component */}
16 <AuxiliaryPanel />
17 </aside>
18 </div>
19 );
20}The three-zone layout has not changed; only what fills the zones has. The middle depends on the address, while the side panels always render and decide for themselves what to show.
Let's assume DashboardLayout is the element of the galaxies/:galaxyId route from the earlier examples. A side panel can then read the address with hooks: useParams, which you already know, and useLocation, which returns an object describing the current address, including the path in its pathname field. You will learn more about useLocation in the upcoming lessons.
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>Galaxy telemetry: {galaxyId}</h3>
10 <p>Current view: {pathname}</p>
11 </div>
12 );
13}At /galaxies/andromeda/systems the panel shows a heading with the andromeda identifier and the full path. The panel itself knows nothing about the child routes: it only reads its own route's params and the address.
Sometimes it is the child route that should decide what appears in the panel. That is what the route handle property is for: any data a route attaches to its match. You read it with the useMatches() hook, which returns all active matches, from the layout down to the deepest child route. The React Router documentation shows this pattern for breadcrumbs. There is one condition: useMatches works only in a data router, that is, when you pass your routes to createBrowserRouter and render the app with <RouterProvider router={router} />. Such a router knows the whole route tree up front. Inside <BrowserRouter> the hook throws an error.
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: any data attached to this route's match
13 handle: { sidebar: () => <SystemsFilter /> },
14 },
15 ],
16 },
17]);Here the routes are described by an array of objects instead of <Route> components, but the structure is the same: a layout with its child routes in children, and index: true plays the role of the index attribute. The only new part is the handle property on the systems route: its sidebar field is a function that returns the panel. Now MissionSidebar goes through the matches starting from the deepest one and uses the first handle.sidebar it finds:
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>Choose a solar system</p>;
8}The copy of the array ([...matches]) is needed because reverse() reverses an array in place. At /galaxies/andromeda/systems the left panel shows SystemsFilter, because the systems route decided so. At /galaxies/andromeda the index route has no handle, so the panel displays the default text, and the layout keeps exactly one Outlet the whole time. I recommend starting with regular components that use useParams and useLocation, and reaching for handle only when each child route needs its own content in the panel. You will meet the data router again when blocking navigation, in the lesson about browser history.
Optimizing Nested Routes
When working with nested routes, it's worth keeping a few optimization techniques in mind:
1. Lazy Loading Components
For improved performance, components can be loaded dynamically only when they are needed:
1import { Suspense, lazy } from 'react';
2import { Routes, Route } from 'react-router-dom';
3
4// Lazy loading components
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 {/* Lazily loaded component */}
14 <Route
15 path="galaxies"
16 element={
17 <Suspense fallback={<div>Loading galaxies...</div>}>
18 <GalaxiesList />
19 </Suspense>
20 }
21 />
22 <Route
23 path="galaxies/:galaxyId/*"
24 element={
25 <Suspense fallback={<div>Loading galaxy data...</div>}>
26 <GalaxyLayout />
27 </Suspense>
28 }
29 />
30 </Route>
31 </Routes>
32 );
33}2. Render Optimization
Render optimization can be especially important with deep nesting, when a URL parameter change at a deep level can cause unnecessary re-rendering of higher-level components:
1import { memo } from 'react';
2import { Outlet, useParams } from 'react-router-dom';
3
4// Optimized component - won't re-render if its props don't change
5const MemoizedGalaxyLayout = memo(function GalaxyLayout() {
6 const { galaxyId } = useParams();
7
8 console.log(`Rendering GalaxyLayout for galaxy ${galaxyId}`);
9
10 return (
11 <div className="galaxy-layout">
12 <h2>Galaxy: {galaxyId}</h2>
13 <Outlet />
14 </div>
15 );
16});Complete Application Example with Nested Routes
Below is an example of a complete space application with nested routes:
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// Main application layout
15function AppLayout() {
16 return (
17 <div className="cosmos-explorer">
18 <header className="main-header">
19 <h1>Cosmic Traveler's Atlas</h1>
20 <MainNavigation />
21 </header>
22
23 <main className="main-content">
24 <Outlet />
25 </main>
26
27 <footer className="main-footer">
28 <p>© 2150 Intergalactic Travel Agency</p>
29 </footer>
30 </div>
31 );
32}
33
34// Main navigation
35function MainNavigation() {
36 return (
37 <nav className="main-nav">
38 <ul>
39 <li><NavLink to="/" end>Command Center</NavLink></li>
40 <li><NavLink to="/galaxies">Galaxies</NavLink></li>
41 <li><NavLink to="/missions">Missions</NavLink></li>
42 <li><NavLink to="/crew">Crew</NavLink></li>
43 </ul>
44 </nav>
45 );
46}
47
48// Home page
49function Dashboard() {
50 return (
51 <div className="dashboard">
52 <h2>Spaceship Command Center</h2>
53 <p>Welcome, Captain! Which location would you like to explore today?</p>
54
55 <div className="dashboard-tiles">
56 <div className="tile">
57 <h3>Galaxies</h3>
58 <p>Explore different galaxies in the universe</p>
59 <Link to="/galaxies" className="tile-link">Go to galaxies</Link>
60 </div>
61
62 <div className="tile">
63 <h3>Missions</h3>
64 <p>Browse current and future space missions</p>
65 <Link to="/missions" className="tile-link">Go to missions</Link>
66 </div>
67
68 <div className="tile">
69 <h3>Crew</h3>
70 <p>Manage spaceship crew members</p>
71 <Link to="/crew" className="tile-link">Go to crew</Link>
72 </div>
73 </div>
74 </div>
75 );
76}
77
78// ===== GALAXIES SECTION =====
79
80// Galaxies layout
81function GalaxiesLayout() {
82 return (
83 <div className="galaxies-section">
84 <h2>Galaxy Exploration</h2>
85 <Outlet />
86 </div>
87 );
88}
89
90// Galaxies list
91function GalaxiesList() {
92 const galaxies = [
93 { id: 'milky-way', name: 'Milky Way', type: 'Spiral' },
94 { id: 'andromeda', name: 'Andromeda', type: 'Spiral' },
95 { id: 'triangulum', name: 'Triangulum Galaxy', type: 'Spiral' },
96 { id: 'large-magellanic', name: 'Large Magellanic Cloud', type: 'Irregular' }
97 ];
98
99 return (
100 <div className="galaxies-list">
101 <h3>Available Galaxies</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 Explore galaxy
109 </Link>
110 </div>
111 ))}
112 </div>
113 </div>
114 );
115}
116
117// Galaxy details layout
118function GalaxyLayout() {
119 const { galaxyId } = useParams();
120 const navigate = useNavigate();
121
122 // Galaxy data simulation (in a real application fetched from API)
123 const galaxyData = {
124 'milky-way': {
125 name: 'Milky Way',
126 type: 'Spiral',
127 diameter: '100,000 light-years',
128 stars: '200-400 billion',
129 systems: [
130 { id: 'solar', name: 'Solar System' },
131 { id: 'alpha-centauri', name: 'Alpha Centauri' },
132 { id: 'sirius', name: 'Sirius' }
133 ]
134 },
135 'andromeda': {
136 name: 'Andromeda',
137 type: 'Spiral',
138 diameter: '220,000 light-years',
139 stars: '1 trillion',
140 systems: [
141 { id: 'system-1', name: 'System M31-A' },
142 { id: 'system-2', name: 'System M31-B' }
143 ]
144 },
145 'triangulum': {
146 name: 'Triangulum Galaxy',
147 type: 'Spiral',
148 diameter: '60,000 light-years',
149 stars: '40 billion',
150 systems: [
151 { id: 'system-1', name: 'System T1' }
152 ]
153 },
154 'large-magellanic': {
155 name: 'Large Magellanic Cloud',
156 type: 'Irregular',
157 diameter: '14,000 light-years',
158 stars: '30 billion',
159 systems: []
160 }
161 };
162
163 const galaxy = galaxyData[galaxyId];
164
165 if (!galaxy) {
166 return (
167 <div className="galaxy-not-found">
168 <h3>Galaxy Not Found</h3>
169 <p>No galaxy found with identifier: {galaxyId}</p>
170 <button onClick={() => navigate('/galaxies')}>
171 Return to galaxy list
172 </button>
173 </div>
174 );
175 }
176
177 return (
178 <div className="galaxy-details">
179 <h3>Galaxy: {galaxy.name}</h3>
180
181 <nav className="galaxy-nav">
182 <NavLink to="" end>Overview</NavLink>
183 <NavLink to="systems">Solar Systems</NavLink>
184 <NavLink to="map">Galaxy Map</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">← Return to galaxy list</Link>
193 </div>
194 </div>
195 );
196}
197
198// Galaxy overview (default view)
199function GalaxyOverview() {
200 const { galaxy } = useOutletContext();
201
202 return (
203 <div className="galaxy-overview">
204 <h4>Overview: {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>Diameter:</th>
213 <td>{galaxy.diameter}</td>
214 </tr>
215 <tr>
216 <th>Number of stars:</th>
217 <td>{galaxy.stars}</td>
218 </tr>
219 <tr>
220 <th>Discovered systems:</th>
221 <td>{galaxy.systems.length}</td>
222 </tr>
223 </tbody>
224 </table>
225
226 <h5>Fun Facts</h5>
227 <p>Unique fun facts about galaxy {galaxy.name} will appear here...</p>
228 </div>
229 );
230}
231
232// Solar systems list in galaxy
233function GalaxySystems() {
234 const { galaxy } = useOutletContext();
235 const { galaxyId } = useParams();
236
237 return (
238 <div className="galaxy-systems">
239 <h4>Solar Systems in {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 Explore system
250 </Link>
251 </li>
252 ))}
253 </ul>
254 ) : (
255 <p>No systems have been discovered in this galaxy yet.</p>
256 )}
257 </div>
258 );
259}
260
261// Galaxy map
262function GalaxyMap() {
263 const { galaxy } = useOutletContext();
264
265 return (
266 <div className="galaxy-map">
267 <h4>Galaxy Map: {galaxy.name}</h4>
268 <div className="map-placeholder">
269 <p>An interactive map of galaxy {galaxy.name} will appear here.</p>
270 <p>Currently under development...</p>
271 </div>
272 </div>
273 );
274}
275
276// ===== SOLAR SYSTEMS SECTION =====
277
278// Solar system details
279function SolarSystemDetails() {
280 const { galaxyId, systemId } = useParams();
281 const navigate = useNavigate();
282
283 // In a real application, data would be fetched based on galaxyId and systemId
284 const systemData = {
285 'solar': {
286 name: 'Solar System',
287 star: 'Sun',
288 planets: 8,
289 age: '4.6 billion years'
290 },
291 'alpha-centauri': {
292 name: 'Alpha Centauri',
293 star: 'Alpha Centauri A and B, Proxima Centauri',
294 planets: 3,
295 age: '4.85 billion years'
296 },
297 'sirius': {
298 name: 'Sirius',
299 star: 'Sirius A and B',
300 planets: 0,
301 age: '230 million years'
302 },
303 'system-1': {
304 name: 'System M31-A',
305 star: 'Star M31-A',
306 planets: 5,
307 age: '2.3 billion years'
308 },
309 'system-2': {
310 name: 'System M31-B',
311 star: 'Star M31-B',
312 planets: 7,
313 age: '3.1 billion years'
314 }
315 };
316
317 const system = systemData[systemId];
318
319 if (!system) {
320 return (
321 <div className="system-not-found">
322 <h4>System Not Found</h4>
323 <p>No solar system found with identifier: {systemId}</p>
324 <button onClick={() => navigate(`/galaxies/${galaxyId}/systems`)}>
325 Return to systems list
326 </button>
327 </div>
328 );
329 }
330
331 return (
332 <div className="system-details">
333 <h4>Solar System: {system.name}</h4>
334
335 <table className="system-stats">
336 <tbody>
337 <tr>
338 <th>Star(s):</th>
339 <td>{system.star}</td>
340 </tr>
341 <tr>
342 <th>Number of planets:</th>
343 <td>{system.planets}</td>
344 </tr>
345 <tr>
346 <th>Age:</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 ← Return to solar systems
355 </Link>
356 </div>
357 </div>
358 );
359}
360
361// ===== MISSIONS SECTION =====
362
363// Missions layout
364function MissionsLayout() {
365 return (
366 <div className="missions-section">
367 <h2>Space Missions</h2>
368 <Outlet />
369 </div>
370 );
371}
372
373// Missions list
374function MissionsList() {
375 const missions = [
376 { id: 'apollo', name: 'Apollo', status: 'Completed' },
377 { id: 'artemis', name: 'Artemis', status: 'In Progress' },
378 { id: 'mars-2030', name: 'Mars 2030', status: 'Planned' }
379 ];
380
381 return (
382 <div className="missions-list">
383 <h3>Space Missions</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// Mission details
396function MissionDetails() {
397 const { missionId } = useParams();
398
399 return (
400 <div className="mission-details">
401 <h3>Mission details: {missionId}</h3>
402 <p>Details for mission {missionId} will appear here...</p>
403 <Link to="/missions">← Return to missions list</Link>
404 </div>
405 );
406}
407
408// ===== CREW SECTION =====
409
410// Crew layout
411function CrewLayout() {
412 return (
413 <div className="crew-section">
414 <h2>Crew Management</h2>
415 <Outlet />
416 </div>
417 );
418}
419
420// Crew members list
421function CrewList() {
422 const crewMembers = [
423 { id: 'crew1', name: 'John Smith', role: 'Captain' },
424 { id: 'crew2', name: 'Anna Johnson', role: 'Navigator' },
425 { id: 'crew3', name: 'Peter Williams', role: 'Engineer' }
426 ];
427
428 return (
429 <div className="crew-list">
430 <h3>Crew Members</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// Crew member details
443function CrewMemberDetails() {
444 const { crewId } = useParams();
445
446 return (
447 <div className="crew-member-details">
448 <h3>Crew member profile: {crewId}</h3>
449 <p>Profile for crew member {crewId} will appear here...</p>
450 <Link to="/crew">← Return to crew list</Link>
451 </div>
452 );
453}
454
455// ===== MAIN APPLICATION COMPONENT =====
456
457function App() {
458 return (
459 <BrowserRouter>
460 <Routes>
461 <Route path="/" element={<AppLayout />}>
462 {/* Home page */}
463 <Route index element={<Dashboard />} />
464
465 {/* Galaxies section */}
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 {/* Missions section */}
477 <Route path="missions" element={<MissionsLayout />}>
478 <Route index element={<MissionsList />} />
479 <Route path=":missionId" element={<MissionDetails />} />
480 </Route>
481
482 {/* Crew section */}
483 <Route path="crew" element={<CrewLayout />}>
484 <Route index element={<CrewList />} />
485 <Route path=":crewId" element={<CrewMemberDetails />} />
486 </Route>
487
488 {/* 404 handling */}
489 <Route path="*" element={<NotFound />} />
490 </Route>
491 </Routes>
492 </BrowserRouter>
493 );
494}
495
496// Component for non-existent paths
497function NotFound() {
498 return (
499 <div className="not-found">
500 <h2>404 - Lost in Space</h2>
501 <p>This path does not lead to any known destination in space.</p>
502 <Link to="/">Return to Command Center</Link>
503 </div>
504 );
505}
506
507export default App;Best Practices for Nested Routes
- Maintain a Logical Hierarchy - The structure of nested routes should reflect the logical organization of the application
- Use Layout Components - Create dedicated layout components for each nesting level
- Optimize Data Passing - Use useOutletContext or React Context for efficient data passing
- Ensure Intuitive Navigation - Always add links that allow easy navigation up and down the hierarchy
- Split Code - Use lazy loading for performance optimization, especially for deeply nested components
- Keep URLs Clean - Design URLs to be readable and logical for the user
- Avoid Too Deep Nesting - Try not to exceed 3-4 nesting levels for clarity
Summary
Nested routes in React Router are a powerful technique for organizing navigation in React applications, allowing for the creation of intuitive, hierarchical user interface structures. Key benefits:
- Logical Organization - Routing structure reflects the logical application hierarchy
- Layout Reuse - Common UI elements can be shared between sub-routes
- Better UX - Users can easily orient themselves in the application structure
- Easier State Management - State can be stored at the appropriate nesting level
Just as in real space exploration, where we travel from a galaxy, through solar systems, to planets and moons - nested routes allow creating intuitive navigation systems that guide the user through different levels of our space application.
Code for this lesson: App.jsx
1import React, { useState } from 'react';
2import { BrowserRouter, Routes, Route, Link, useNavigate, useLocation } from 'react-router-dom';
3
4// Login page
5function LoginPage() {
6 const [username, setUsername] = useState('');
7 const navigate = useNavigate();
8
9 const handleLogin = (e) => {
10 e.preventDefault();
11 if (username.trim()) {
12 // Programmatic navigation with state passed
13 navigate('/dashboard', { state: { username } });
14 }
15 };
16
17 return (
18 <div className="page login">
19 <h1>Login Panel</h1>
20 <form onSubmit={handleLogin}>
21 <input
22 type="text"
23 value={username}
24 onChange={(e) => setUsername(e.target.value)}
25 placeholder="Enter username"
26 />
27 <button type="submit">Log in</button>
28 </form>
29 </div>
30 );
31}
32
33// Dashboard - using useLocation to read state
34function Dashboard() {
35 const location = useLocation();
36 const navigate = useNavigate();
37 const username = location.state?.username || 'Guest';
38
39 const handleLogout = () => {
40 // Navigation with replace: true - doesn't add to history
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>Welcome, {username}!</h1>
51 <p>Your space mission control panel</p>
52
53 <div className="missions">
54 <h2>Available missions:</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>Programmatic navigation:</h2>
64 <button onClick={() => navigate(-1)}>β Back (go -1)</button>
65 <button onClick={() => navigate(1)}>Forward (go 1) β</button>
66 <button onClick={handleLogout} className="logout">Log out</button>
67 </div>
68 </div>
69 );
70}
71
72// Mission page
73function MissionPage() {
74 const navigate = useNavigate();
75 const location = useLocation();
76
77 // Get mission ID from URL
78 const missionId = location.pathname.split('/').pop();
79
80 return (
81 <div className="page mission">
82 <h1>Mission: {missionId.toUpperCase()}</h1>
83 <p>Mission details {missionId}</p>
84
85 <div className="location-info">
86 <h3>Location information:</h3>
87 <pre>{JSON.stringify(location, null, 2)}</pre>
88 </div>
89
90 <button onClick={() => navigate('/dashboard', { state: location.state })}>
91 β Back to panel
92 </button>
93 </div>
94 );
95}
96
97function App() {
98 return (
99 <BrowserRouter>
100 <div className="app">
101 <nav>
102 <Link to="/">Login</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;Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. The useParams hook allows you to:
2. The useSearchParams hook is used for:
Hands-on tasks in the game
- Code editor
Nested routes with Outlet
- Horizontal ordering
Arrange the useParams destructuring elements from left to right:
- Code editor
Search params and filtering
- Vertical ordering
Arrange the steps for configuring nested routes: