Kurs JavaScript i React · Moduł 6: React Router
Zagnieżdżone ścieżki w intergalaktycznej nawigacji
W tej lekcji9
W naszej kosmicznej podróży przez świat React Router, nadszedł czas na zgłębienie koncepcji zagnieżdżonych ścieżek. Podobnie jak w rzeczywistej przestrzeni kosmicznej, gdzie mamy galaktyki zawierające układy słoneczne, które z kolei zawierają planety z ich księżycami - w aplikacjach React możemy tworzyć hierarchiczne struktury nawigacyjne.
Czym są zagnieżdżone ścieżki?
Zagnieżdżone ścieżki to technika organizowania routingu w aplikacji, gdzie jedne ścieżki (komponenty) zawierają w sobie inne ścieżki (komponenty). Pozwala to na tworzenie bardziej intuicyjnych, hierarchicznych struktur nawigacyjnych, które odzwierciedlają logiczny układ aplikacji.
Zagnieżdżone ścieżki są szczególnie przydatne w większych aplikacjach, gdzie mamy wiele podsekcji z własnymi widokami.
Implementacja zagnieżdżonych ścieżek w React Router
W React Router v6 zagnieżdżone ścieżki implementuje się poprzez umieszczanie komponentów Route wewnątrz innych komponentów Route oraz używanie komponentu Outlet do określenia, gdzie mają być renderowane komponenty podścieżek.
Podstawowa struktura zagnieżdżonych ścieżek
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}W powyższym przykładzie:
Layoutto główny komponent, który będzie zawierał wszystkie zagnieżdżone widokiGalaxyLayoutto komponent, który będzie służył jako "kontener" dla szczegółów galaktyki- Atrybut
indexokreśla, co ma być renderowane, gdy URL pasuje dokładnie do ścieżki rodzica
Komponent Outlet
Kluczowym elementem zagnieżdżonych ścieżek jest komponent Outlet, który działa jak "placeholder" dla komponentów potomnych. Określa on, gdzie w komponencie nadrzędnym mają być renderowane komponenty podścieżek:
1import { Outlet, Link } from 'react-router-dom';
2
3function Layout() {
4 return (
5 <div className="app-layout">
6 <header>
7 <h1>Kosmiczny Atlas</h1>
8 <nav>
9 <Link to="/">Strona główna</Link>
10 <Link to="/galaxies">Galaktyki</Link>
11 </nav>
12 </header>
13
14 <main>
15 {/* Tutaj będą renderowane komponenty podścieżek */}
16 <Outlet />
17 </main>
18
19 <footer>© 2150 Międzygalaktyczne Centrum Nawigacji</footer>
20 </div>
21 );
22}
23
24function GalaxyLayout() {
25 const { galaxyId } = useParams();
26
27 return (
28 <div className="galaxy-layout">
29 <h2>Galaktyka: {galaxyId}</h2>
30 <nav className="galaxy-navigation">
31 <Link to={`/galaxies/${galaxyId}`}>Przegląd</Link>
32 <Link to={`/galaxies/${galaxyId}/systems`}>Układy słoneczne</Link>
33 </nav>
34
35 <div className="galaxy-content">
36 {/* Tutaj będą renderowane komponenty podścieżek galaktyki */}
37 <Outlet />
38 </div>
39 </div>
40 );
41}Względne i indeksowe ścieżki
React Router v6 wprowadził kilka użytecznych funkcji dla lepszej obsługi zagnieżdżonych ścieżek:
1. Ścieżki indeksowe (index routes)
Ścieżka indeksowa określa, co powinno być renderowane, gdy URL pasuje dokładnie do ścieżki nadrzędnej:
1<Route path="galaxies" element={<GalaxiesLayout />}>
2 {/* To będzie renderowane dla /galaxies */}
3 <Route index element={<GalaxiesList />} />
4
5 {/* To będzie renderowane dla /galaxies/:galaxyId */}
6 <Route path=":galaxyId" element={<GalaxyDetails />} />
7</Route>2. Względne linki
W komponentach zagnieżdżonych możemy używać względnych linków, które są rozwiązywane względem bieżącej ścieżki:
1import { Link, useParams } from 'react-router-dom';
2
3function GalaxySystemsList() {
4 const { galaxyId } = useParams();
5
6 return (
7 <div>
8 <h3>Układy słoneczne w galaktyce {galaxyId}</h3>
9 <ul>
10 {systems.map(system => (
11 <li key={system.id}>
12 {/* Względny link - zostanie rozwiązany do /galaxies/:galaxyId/systems/:systemId */}
13 <Link to={system.id}>{system.name}</Link>
14 </li>
15 ))}
16 </ul>
17
18 {/* Względny link "w górę" - cofnij się o jeden poziom */}
19 <Link to="..">Powrót do przeglądu galaktyki</Link>
20 </div>
21 );
22}Przekazywanie kontekstu między poziomami zagnieżdżenia
Jednym z wyzwań przy pracy z zagnieżdżonymi ścieżkami jest przekazywanie danych między różnymi poziomami zagnieżdżenia. React Router v6 oferuje kilka rozwiązań:
1. Hooki useOutletContext i useOutlet
Hook useOutletContext pozwala na przekazywanie danych z komponentu nadrzędnego do podrzędnego:
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 // Pobierz dane galaktyki
9 fetchGalaxy(galaxyId).then(data => setGalaxy(data));
10 }, [galaxyId]);
11
12 // Przekaż dane galaktyki do komponentów potomnych
13 return (
14 <div className="galaxy-layout">
15 <h2>{galaxy?.name || 'Ładowanie...'}</h2>
16 <Outlet context={{ galaxy, updateGalaxy: setGalaxy }} />
17 </div>
18 );
19}
20
21function GalaxyOverview() {
22 // Odbierz kontekst z komponentu nadrzędnego
23 const { galaxy, updateGalaxy } = useOutletContext();
24
25 if (!galaxy) return <div>Ładowanie danych galaktyki...</div>;
26
27 return (
28 <div className="galaxy-overview">
29 <h3>Przegląd galaktyki {galaxy.name}</h3>
30 <p>Typ: {galaxy.type}</p>
31 <p>Liczba układów: {galaxy.systemsCount}</p>
32 {/* Reszta komponentu */}
33 </div>
34 );
35}2. Wspólny stan z React Context
Dla bardziej złożonych scenariuszy można wykorzystać React Context do udostępniania stanu między komponentami:
1import { createContext, useContext, useState } from 'react';
2import { Outlet } from 'react-router-dom';
3
4// Stwórz kontekst
5const GalaxyContext = createContext();
6
7// Hook do łatwego dostępu do kontekstu
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 // Wartość kontekstu
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>Eksploracja Galaktyk</h2>
40 <Outlet />
41 </div>
42 </GalaxyProvider>
43 );
44}
45
46// Użycie w komponencie potomnym
47function SolarSystemDetail() {
48 const { favoritesSystems, addFavoriteSystem } = useGalaxy();
49 const { systemId } = useParams();
50
51 // Reszta komponentu
52}Zaawansowane wzorce zagnieżdżania ścieżek
Zagnieżdżone ścieżki mogą być wykorzystywane do implementacji różnych wzorców nawigacyjnych:
1. Layout wielopoziomowy
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>W tym przykładzie:
MainLayoutzawiera ogólną strukturę stronyAuthLayoutobsługuje uwierzytelnianie dla chronionych ścieżekSettingsLayouttworzy dodatkowy poziom nawigacji dla ustawień
2. Dynamiczne segmenty i zagnieżdżenie
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>Ta struktura tworzy głębokie zagnieżdżenie ścieżek, które odzwierciedla hierarchię galaktyka > układ słoneczny > planeta.
3. Wiele slotów w jednym layoucie
Panel dowodzenia galaktyki ma trzy strefy: nawigację po lewej, główny widok w środku i telemetrię po prawej. Jeśli znasz nazwane router-outlet z Angulara albo nazwane widoki z Vue Router, możesz szukać w React Router czegoś podobnego. Nazwanych outletów tu nie ma: Outlet przyjmuje tylko jeden prop, context, i renderuje komponent dopasowanej podścieżki (albo nic, gdy żadna nie pasuje). Gdybyś napisał <Outlet name="sidebar" />, React Router zignorowałby nieznany prop i wyrenderował w tym miejscu tę samą podścieżkę co główny Outlet.
Zamiast nazwanych slotów layout ma jeden Outlet na główny widok, a pozostałe strefy wypełniają zwykłe komponenty:
1import { Outlet } from 'react-router-dom';
2
3function DashboardLayout() {
4 return (
5 <div className="dashboard">
6 <aside className="sidebar">
7 {/* Zwykły komponent nawigacji bocznej */}
8 <MissionSidebar />
9 </aside>
10 <div className="main-content">
11 {/* Jedyny Outlet: tu trafia dopasowana podścieżka */}
12 <Outlet />
13 </div>
14 <aside className="auxiliary-panel">
15 {/* Zwykły komponent panelu pomocniczego */}
16 <AuxiliaryPanel />
17 </aside>
18 </div>
19 );
20}Układ trzech stref się nie zmienił, zmieniło się tylko to, kto je wypełnia. Środek zależy od adresu, a panele boczne renderują się zawsze i same decydują, co pokazać.
Załóżmy, że DashboardLayout jest elementem trasy galaxies/:galaxyId z wcześniejszych przykładów. Panel boczny może wtedy czytać adres hookami: znanym Ci useParams oraz useLocation, który zwraca obiekt z bieżącym adresem, m.in. ze ścieżką w polu pathname. Więcej o useLocation dowiesz się w kolejnych lekcjach.
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>Telemetria galaktyki {galaxyId}</h3>
10 <p>Aktualny widok: {pathname}</p>
11 </div>
12 );
13}Na adresie /galaxies/andromeda/systems panel pokaże nagłówek z identyfikatorem andromeda i pełną ścieżkę. Sam panel nie zna konfiguracji podścieżek: czyta tylko parametry swojej trasy i adres.
Czasem to podścieżka powinna zdecydować, co pojawi się w panelu. Służy do tego właściwość trasy handle, czyli dowolne dane, które trasa dołącza do swojego dopasowania. Odczytuje się je hookiem useMatches(), który zwraca wszystkie aktywne dopasowania, od layoutu do najgłębszej podścieżki. Dokumentacja React Router pokazuje ten wzorzec przy okruszkach nawigacyjnych (breadcrumbs). Jest jeden warunek: useMatches działa tylko w routerze danych (data router), czyli gdy trasy przekazujesz funkcji createBrowserRouter, a aplikację renderujesz przez <RouterProvider router={router} />. Taki router zna całe drzewo tras z góry. W <BrowserRouter> hook rzuci błąd.
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: dowolne dane dołączone do dopasowania tej trasy
13 handle: { sidebar: () => <SystemsFilter /> },
14 },
15 ],
16 },
17]);Trasy opisuje tu tablica obiektów zamiast komponentów <Route>, ale struktura jest ta sama: layout z podścieżkami w children, a index: true pełni rolę atrybutu index. Nowa jest tylko właściwość handle przy trasie systems: jej pole sidebar to funkcja zwracająca panel. Teraz MissionSidebar przegląda dopasowania od najgłębszego i używa pierwszego znalezionego handle.sidebar:
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>Wybierz układ słoneczny</p>;
8}Kopia tablicy ([...matches]) jest potrzebna, bo reverse() odwraca tablicę w miejscu. Na /galaxies/andromeda/systems lewy panel pokaże SystemsFilter, bo tak zdecydowała trasa systems. Na /galaxies/andromeda trasa indeksowa nie ma handle, więc panel wyświetli tekst domyślny, a layout przez cały czas ma dokładnie jeden Outlet. Polecam zaczynać od zwykłych komponentów z useParams i useLocation, a po handle sięgać dopiero wtedy, gdy każda podścieżka ma mieć w panelu własną treść. Z routerem danych spotkasz się ponownie przy blokowaniu nawigacji, w lekcji o historii przeglądarki.
Optymalizacja zagnieżdżonych ścieżek
Przy pracy z zagnieżdżonymi ścieżkami, warto pamiętać o kilku technikach optymalizacji:
1. Lazy Loading komponentów
Dla poprawy wydajności, komponenty mogą być ładowane dynamicznie tylko wtedy, gdy są potrzebne:
1import { Suspense, lazy } from 'react';
2import { Routes, Route } from 'react-router-dom';
3
4// Lazy loading komponentów
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 {/* Komponent ładowany leniwie */}
14 <Route
15 path="galaxies"
16 element={
17 <Suspense fallback={<div>Ładowanie galaktyk...</div>}>
18 <GalaxiesList />
19 </Suspense>
20 }
21 />
22 <Route
23 path="galaxies/:galaxyId/*"
24 element={
25 <Suspense fallback={<div>Ładowanie danych galaktyki...</div>}>
26 <GalaxyLayout />
27 </Suspense>
28 }
29 />
30 </Route>
31 </Routes>
32 );
33}2. Optymalizacja renderowania
Optymalizacja renderowania może być szczególnie ważna przy głębokim zagnieżdżeniu, gdy zmiana parametru URL na głębokim poziomie może spowodować zbędne rerenderowanie komponentów wyższego poziomu:
1import { memo } from 'react';
2import { Outlet, useParams } from 'react-router-dom';
3
4// Komponent zoptymalizowany - nie będzie się rerenderował, jeśli jego props się nie zmienią
5const MemoizedGalaxyLayout = memo(function GalaxyLayout() {
6 const { galaxyId } = useParams();
7
8 console.log(`Renderowanie GalaxyLayout dla galaktyki ${galaxyId}`);
9
10 return (
11 <div className="galaxy-layout">
12 <h2>Galaktyka: {galaxyId}</h2>
13 <Outlet />
14 </div>
15 );
16});Przykład kompletnej aplikacji z zagnieżdżonymi ścieżkami
Poniżej znajduje się przykład kompletnej aplikacji kosmicznej z zagnieżdżonymi ścieżkami:
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// Główny layout aplikacji
15function AppLayout() {
16 return (
17 <div className="cosmos-explorer">
18 <header className="main-header">
19 <h1>Kosmiczny Atlas Podróżnika</h1>
20 <MainNavigation />
21 </header>
22
23 <main className="main-content">
24 <Outlet />
25 </main>
26
27 <footer className="main-footer">
28 <p>© 2150 Międzygalaktyczna Agencja Podróży</p>
29 </footer>
30 </div>
31 );
32}
33
34// Główna nawigacja
35function MainNavigation() {
36 return (
37 <nav className="main-nav">
38 <ul>
39 <li><NavLink to="/" end>Centrum Dowodzenia</NavLink></li>
40 <li><NavLink to="/galaxies">Galaktyki</NavLink></li>
41 <li><NavLink to="/missions">Misje</NavLink></li>
42 <li><NavLink to="/crew">Załoga</NavLink></li>
43 </ul>
44 </nav>
45 );
46}
47
48// Strona główna
49function Dashboard() {
50 return (
51 <div className="dashboard">
52 <h2>Centrum Dowodzenia Statku Kosmicznego</h2>
53 <p>Witaj, Kapitanie! Jaką lokalizację chcesz dziś eksplorować?</p>
54
55 <div className="dashboard-tiles">
56 <div className="tile">
57 <h3>Galaktyki</h3>
58 <p>Eksploruj różne galaktyki we wszechświecie</p>
59 <Link to="/galaxies" className="tile-link">Przejdź do galaktyk</Link>
60 </div>
61
62 <div className="tile">
63 <h3>Misje</h3>
64 <p>Przeglądaj aktualne i przyszłe misje kosmiczne</p>
65 <Link to="/missions" className="tile-link">Przejdź do misji</Link>
66 </div>
67
68 <div className="tile">
69 <h3>Załoga</h3>
70 <p>Zarządzaj członkami załogi statku kosmicznego</p>
71 <Link to="/crew" className="tile-link">Przejdź do załogi</Link>
72 </div>
73 </div>
74 </div>
75 );
76}
77
78// ===== SEKCJA GALAKTYK =====
79
80// Layout dla galaktyk
81function GalaxiesLayout() {
82 return (
83 <div className="galaxies-section">
84 <h2>Eksploracja Galaktyk</h2>
85 <Outlet />
86 </div>
87 );
88}
89
90// Lista galaktyk
91function GalaxiesList() {
92 const galaxies = [
93 { id: 'milky-way', name: 'Droga Mleczna', type: 'Spiralna' },
94 { id: 'andromeda', name: 'Andromeda', type: 'Spiralna' },
95 { id: 'triangulum', name: 'Galaktyka Trójkąta', type: 'Spiralna' },
96 { id: 'large-magellanic', name: 'Wielki Obłok Magellana', type: 'Nieregularna' }
97 ];
98
99 return (
100 <div className="galaxies-list">
101 <h3>Dostępne Galaktyki</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>Typ: {galaxy.type}</p>
107 <Link to={galaxy.id} className="galaxy-link">
108 Eksploruj galaktykę
109 </Link>
110 </div>
111 ))}
112 </div>
113 </div>
114 );
115}
116
117// Layout szczegółów galaktyki
118function GalaxyLayout() {
119 const { galaxyId } = useParams();
120 const navigate = useNavigate();
121
122 // Symulacja danych galaktyki (w prawdziwej aplikacji pobierane z API)
123 const galaxyData = {
124 'milky-way': {
125 name: 'Droga Mleczna',
126 type: 'Spiralna',
127 diameter: '100,000 lat świetlnych',
128 stars: '200-400 miliardów',
129 systems: [
130 { id: 'solar', name: 'Układ Słoneczny' },
131 { id: 'alpha-centauri', name: 'Alfa Centauri' },
132 { id: 'sirius', name: 'Syriusz' }
133 ]
134 },
135 'andromeda': {
136 name: 'Andromeda',
137 type: 'Spiralna',
138 diameter: '220,000 lat świetlnych',
139 stars: '1 bilion',
140 systems: [
141 { id: 'system-1', name: 'Układ M31-A' },
142 { id: 'system-2', name: 'Układ M31-B' }
143 ]
144 },
145 'triangulum': {
146 name: 'Galaktyka Trójkąta',
147 type: 'Spiralna',
148 diameter: '60,000 lat świetlnych',
149 stars: '40 miliardów',
150 systems: [
151 { id: 'system-1', name: 'Układ T1' }
152 ]
153 },
154 'large-magellanic': {
155 name: 'Wielki Obłok Magellana',
156 type: 'Nieregularna',
157 diameter: '14,000 lat świetlnych',
158 stars: '30 miliardów',
159 systems: []
160 }
161 };
162
163 const galaxy = galaxyData[galaxyId];
164
165 if (!galaxy) {
166 return (
167 <div className="galaxy-not-found">
168 <h3>Galaktyka nie znaleziona</h3>
169 <p>Nie znaleziono galaktyki o identyfikatorze: {galaxyId}</p>
170 <button onClick={() => navigate('/galaxies')}>
171 Powrót do listy galaktyk
172 </button>
173 </div>
174 );
175 }
176
177 return (
178 <div className="galaxy-details">
179 <h3>Galaktyka: {galaxy.name}</h3>
180
181 <nav className="galaxy-nav">
182 <NavLink to="" end>Przegląd</NavLink>
183 <NavLink to="systems">Układy Słoneczne</NavLink>
184 <NavLink to="map">Mapa Galaktyki</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">← Powrót do listy galaktyk</Link>
193 </div>
194 </div>
195 );
196}
197
198// Przegląd galaktyki (domyślny widok)
199function GalaxyOverview() {
200 const { galaxy } = useOutletContext();
201
202 return (
203 <div className="galaxy-overview">
204 <h4>Przegląd: {galaxy.name}</h4>
205 <table className="galaxy-stats">
206 <tbody>
207 <tr>
208 <th>Typ:</th>
209 <td>{galaxy.type}</td>
210 </tr>
211 <tr>
212 <th>Średnica:</th>
213 <td>{galaxy.diameter}</td>
214 </tr>
215 <tr>
216 <th>Liczba gwiazd:</th>
217 <td>{galaxy.stars}</td>
218 </tr>
219 <tr>
220 <th>Odkryte układy:</th>
221 <td>{galaxy.systems.length}</td>
222 </tr>
223 </tbody>
224 </table>
225
226 <h5>Ciekawostki</h5>
227 <p>Tutaj znajdą się unikalne ciekawostki o galaktyce {galaxy.name}...</p>
228 </div>
229 );
230}
231
232// Lista układów słonecznych w galaktyce
233function GalaxySystems() {
234 const { galaxy } = useOutletContext();
235 const { galaxyId } = useParams();
236
237 return (
238 <div className="galaxy-systems">
239 <h4>Układy Słoneczne w {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 Eksploruj układ
250 </Link>
251 </li>
252 ))}
253 </ul>
254 ) : (
255 <p>Nie odkryto jeszcze żadnych układów w tej galaktyce.</p>
256 )}
257 </div>
258 );
259}
260
261// Mapa galaktyki
262function GalaxyMap() {
263 const { galaxy } = useOutletContext();
264
265 return (
266 <div className="galaxy-map">
267 <h4>Mapa Galaktyki: {galaxy.name}</h4>
268 <div className="map-placeholder">
269 <p>Tutaj będzie interaktywna mapa galaktyki {galaxy.name}.</p>
270 <p>Obecnie w fazie rozwoju...</p>
271 </div>
272 </div>
273 );
274}
275
276// ===== SEKCJA UKŁADÓW SŁONECZNYCH =====
277
278// Szczegóły układu słonecznego
279function SolarSystemDetails() {
280 const { galaxyId, systemId } = useParams();
281 const navigate = useNavigate();
282
283 // W prawdziwej aplikacji, dane byłyby pobierane na podstawie galaxyId i systemId
284 const systemData = {
285 'solar': {
286 name: 'Układ Słoneczny',
287 star: 'Słońce',
288 planets: 8,
289 age: '4.6 miliarda lat'
290 },
291 'alpha-centauri': {
292 name: 'Alfa Centauri',
293 star: 'Alfa Centauri A i B, Proxima Centauri',
294 planets: 3,
295 age: '4.85 miliarda lat'
296 },
297 'sirius': {
298 name: 'Syriusz',
299 star: 'Syriusz A i B',
300 planets: 0,
301 age: '230 milionów lat'
302 },
303 'system-1': {
304 name: 'Układ M31-A',
305 star: 'Gwiazda M31-A',
306 planets: 5,
307 age: '2.3 miliarda lat'
308 },
309 'system-2': {
310 name: 'Układ M31-B',
311 star: 'Gwiazda M31-B',
312 planets: 7,
313 age: '3.1 miliarda lat'
314 }
315 };
316
317 const system = systemData[systemId];
318
319 if (!system) {
320 return (
321 <div className="system-not-found">
322 <h4>Układ nie znaleziony</h4>
323 <p>Nie znaleziono układu słonecznego o identyfikatorze: {systemId}</p>
324 <button onClick={() => navigate(`/galaxies/${galaxyId}/systems`)}>
325 Powrót do listy układów
326 </button>
327 </div>
328 );
329 }
330
331 return (
332 <div className="system-details">
333 <h4>Układ Słoneczny: {system.name}</h4>
334
335 <table className="system-stats">
336 <tbody>
337 <tr>
338 <th>Gwiazda(y):</th>
339 <td>{system.star}</td>
340 </tr>
341 <tr>
342 <th>Liczba planet:</th>
343 <td>{system.planets}</td>
344 </tr>
345 <tr>
346 <th>Wiek:</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 ← Powrót do układów słonecznych
355 </Link>
356 </div>
357 </div>
358 );
359}
360
361// ===== SEKCJA MISJI =====
362
363// Layout misji
364function MissionsLayout() {
365 return (
366 <div className="missions-section">
367 <h2>Misje Kosmiczne</h2>
368 <Outlet />
369 </div>
370 );
371}
372
373// Lista misji
374function MissionsList() {
375 const missions = [
376 { id: 'apollo', name: 'Apollo', status: 'Zakończona' },
377 { id: 'artemis', name: 'Artemis', status: 'W trakcie' },
378 { id: 'mars-2030', name: 'Mars 2030', status: 'Planowana' }
379 ];
380
381 return (
382 <div className="missions-list">
383 <h3>Misje kosmiczne</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// Szczegóły misji
396function MissionDetails() {
397 const { missionId } = useParams();
398
399 return (
400 <div className="mission-details">
401 <h3>Szczegóły misji: {missionId}</h3>
402 <p>Tutaj będą szczegóły misji {missionId}...</p>
403 <Link to="/missions">← Powrót do listy misji</Link>
404 </div>
405 );
406}
407
408// ===== SEKCJA ZAŁOGI =====
409
410// Layout załogi
411function CrewLayout() {
412 return (
413 <div className="crew-section">
414 <h2>Zarządzanie Załogą</h2>
415 <Outlet />
416 </div>
417 );
418}
419
420// Lista członków załogi
421function CrewList() {
422 const crewMembers = [
423 { id: 'crew1', name: 'Jan Kowalski', role: 'Kapitan' },
424 { id: 'crew2', name: 'Anna Nowak', role: 'Nawigator' },
425 { id: 'crew3', name: 'Piotr Wiśniewski', role: 'Inżynier' }
426 ];
427
428 return (
429 <div className="crew-list">
430 <h3>Członkowie załogi</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// Szczegóły członka załogi
443function CrewMemberDetails() {
444 const { crewId } = useParams();
445
446 return (
447 <div className="crew-member-details">
448 <h3>Profil członka załogi: {crewId}</h3>
449 <p>Tutaj będzie profil członka załogi {crewId}...</p>
450 <Link to="/crew">← Powrót do listy załogi</Link>
451 </div>
452 );
453}
454
455// ===== GŁÓWNY KOMPONENT APLIKACJI =====
456
457function App() {
458 return (
459 <BrowserRouter>
460 <Routes>
461 <Route path="/" element={<AppLayout />}>
462 {/* Strona główna */}
463 <Route index element={<Dashboard />} />
464
465 {/* Sekcja galaktyk */}
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 {/* Sekcja misji */}
477 <Route path="missions" element={<MissionsLayout />}>
478 <Route index element={<MissionsList />} />
479 <Route path=":missionId" element={<MissionDetails />} />
480 </Route>
481
482 {/* Sekcja załogi */}
483 <Route path="crew" element={<CrewLayout />}>
484 <Route index element={<CrewList />} />
485 <Route path=":crewId" element={<CrewMemberDetails />} />
486 </Route>
487
488 {/* Obsługa 404 */}
489 <Route path="*" element={<NotFound />} />
490 </Route>
491 </Routes>
492 </BrowserRouter>
493 );
494}
495
496// Komponent dla nieistniejących ścieżek
497function NotFound() {
498 return (
499 <div className="not-found">
500 <h2>404 - Zagubiony w kosmosie</h2>
501 <p>Ta ścieżka nie prowadzi do żadnego znanego celu w kosmosie.</p>
502 <Link to="/">Powrót do Centrum Dowodzenia</Link>
503 </div>
504 );
505}
506
507export default App;Najlepsze praktyki dla zagnieżdżonych ścieżek
- Utrzymuj Logiczną Hierarchię - Struktura zagnieżdżonych ścieżek powinna odzwierciedlać logiczną organizację aplikacji
- Używaj Komponentów Layout - Twórz dedykowane komponenty layout dla każdego poziomu zagnieżdżenia
- Optymalizuj Przekazywanie Danych - Używaj useOutletContext lub React Context dla efektywnego przekazywania danych
- Zapewnij Intuicyjną Nawigację - Zawsze dodawaj linki pozwalające na łatwą nawigację w górę i w dół hierarchii
- Dziel Kod - Używaj lazy loading dla optymalizacji wydajności, szczególnie dla głęboko zagnieżdżonych komponentów
- Utrzymuj Czystość URL - Projektuj URL-e tak, aby były czytelne i logiczne dla użytkownika
- Unikaj Zbyt Głębokiego Zagnieżdżenia - Staraj się nie przekraczać 3-4 poziomów zagnieżdżenia dla zachowania przejrzystości
Podsumowanie
Zagnieżdżone ścieżki w React Router to potężna technika organizacji nawigacji w aplikacjach React, pozwalająca na tworzenie intuicyjnych, hierarchicznych struktur interfejsu użytkownika. Kluczowe korzyści:
- Logiczna Organizacja - Struktura routingu odzwierciedla logiczną hierarchię aplikacji
- Ponowne Użycie Layoutów - Wspólne elementy UI mogą być współdzielone między podścieżkami
- Lepsze UX - Użytkownicy mogą łatwo orientować się w strukturze aplikacji
- Łatwiejsze Zarządzanie Stanem - Stan może być przechowywany na odpowiednim poziomie zagnieżdżenia
Podobnie jak w rzeczywistej eksploracji kosmosu, gdzie podróżujemy od galaktyki, przez układy słoneczne, do planet i księżyców - zagnieżdżone ścieżki pozwalają tworzyć intuicyjne systemy nawigacji, które prowadzą użytkownika przez różne poziomy naszej aplikacji kosmicznej.
Kod do tej lekcji: App.jsx
1import React, { useState } from 'react';
2import { BrowserRouter, Routes, Route, Link, useNavigate, useLocation } from 'react-router-dom';
3
4// Strona logowania
5function LoginPage() {
6 const [username, setUsername] = useState('');
7 const navigate = useNavigate();
8
9 const handleLogin = (e) => {
10 e.preventDefault();
11 if (username.trim()) {
12 // Nawigacja programowa z przekazaniem state
13 navigate('/dashboard', { state: { username } });
14 }
15 };
16
17 return (
18 <div className="page login">
19 <h1>Panel Logowania</h1>
20 <form onSubmit={handleLogin}>
21 <input
22 type="text"
23 value={username}
24 onChange={(e) => setUsername(e.target.value)}
25 placeholder="Wprowadź nazwę użytkownika"
26 />
27 <button type="submit">Zaloguj się</button>
28 </form>
29 </div>
30 );
31}
32
33// Dashboard - używamy useLocation do odczytania state
34function Dashboard() {
35 const location = useLocation();
36 const navigate = useNavigate();
37 const username = location.state?.username || 'Gość';
38
39 const handleLogout = () => {
40 // Nawigacja z replace: true - nie dodaje do historii
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>Witaj, {username}!</h1>
51 <p>Twój panel kontroli misji kosmicznych</p>
52
53 <div className="missions">
54 <h2>Dostępne misje:</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>Nawigacja programowa:</h2>
64 <button onClick={() => navigate(-1)}>← Cofnij (go -1)</button>
65 <button onClick={() => navigate(1)}>Dalej (go 1) →</button>
66 <button onClick={handleLogout} className="logout">Wyloguj</button>
67 </div>
68 </div>
69 );
70}
71
72// Strona misji
73function MissionPage() {
74 const navigate = useNavigate();
75 const location = useLocation();
76
77 // Pobierz ID misji z URL
78 const missionId = location.pathname.split('/').pop();
79
80 return (
81 <div className="page mission">
82 <h1>Misja: {missionId.toUpperCase()}</h1>
83 <p>Szczegóły misji {missionId}</p>
84
85 <div className="location-info">
86 <h3>Informacje o lokalizacji:</h3>
87 <pre>{JSON.stringify(location, null, 2)}</pre>
88 </div>
89
90 <button onClick={() => navigate('/dashboard', { state: location.state })}>
91 ← Wróć do panelu
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;Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Hook useParams pozwala na:
2. Hook useSearchParams służy do:
Zadania praktyczne w grze
- Edytor kodu
Zagnieżdżone trasy z Outlet
- Układanie w poziomie
Ułóż elementy destrukturyzacji useParams od lewej do prawej:
- Edytor kodu
Search params i filtrowanie
- Układanie w pionie
Uporządkuj kroki konfiguracji zagnieżdżonych tras (nested routes):