Kurs JavaScript i React · Moduł 6: React Router

Kompletna architektura routingu - Centrum Dowodzenia Nawigacją

5 min czytania
W tej lekcji9

Nawigatorze, dotarliśmy do ostatniego etapu naszej podróży przez Pas Asteroid! Czas zebrać wszystkie techniki nawigacyjne, które poznaliśmy, i zbudować kompletny system routingu - prawdziwe Centrum Dowodzenia Nawigacją dla naszej kosmicznej floty.

W tej lekcji zobaczymy, jak wszystkie elementy React Router współpracują ze sobą w jednej, spójnej aplikacji.

Architektura kompletnej aplikacji z routingiem

Budując aplikację z pełnym routingiem, musimy zaplanować kilka warstw:

  1. Warstwa uwierzytelniania - AuthContext + ProtectedRoute
  2. Warstwa nawigacji - NavLink z aktywnymi stylami
  3. Warstwa tras - zagnieżdżone Routes z Outlet
  4. Warstwa parametrów - dynamiczne ścieżki z useParams
  5. Warstwa przekierowań - obsługa 404 i zabezpieczeń

Struktura folderów

W większych aplikacjach warto zorganizować pliki związane z routingiem w logiczną strukturę:

1src/
2├── App.jsx              // Główny komponent z BrowserRouter
3├── routes/
4│   ├── AppRoutes.jsx    // Definicja wszystkich tras
5│   └── ProtectedRoute.jsx
6├── context/
7│   └── AuthContext.jsx  // Kontekst uwierzytelniania
8├── layouts/
9│   ├── MainLayout.jsx   // Layout z nawigacją i Outlet
10│   └── DashboardLayout.jsx
11├── pages/
12│   ├── HomePage.jsx
13│   ├── LoginPage.jsx
14│   ├── MissionDetail.jsx
15│   └── NotFound.jsx
16└── components/
17    └── Navigation.jsx   // NavLink menu

Taka organizacja pozwala łatwo zarządzać nawet rozbudowanymi aplikacjami z dziesiątkami tras.

Łączenie AuthContext z routingiem

Centralnym elementem jest AuthProvider owijający całą aplikację:

1function AuthProvider({ children }) {
2  const [user, setUser] = useState(null);
3
4  const login = (name) => setUser({ name });
5  const logout = () => setUser(null);
6
7  return (
8    <AuthContext.Provider value={{ user, login, logout }}>
9      {children}
10    </AuthContext.Provider>
11  );
12}
13
14// W App.jsx:
15function App() {
16  return (
17    <AuthProvider>
18      <BrowserRouter>
19        <Navigation />
20        <Routes>
21          {/* trasy publiczne i chronione */}
22        </Routes>
23      </BrowserRouter>
24    </AuthProvider>
25  );
26}

AuthProvider musi być na zewnątrz BrowserRouter, aby kontekst uwierzytelniania był dostępny we wszystkich komponentach tras.

Komponent nawigacji łączy NavLink (z aktywnymi stylami) z AuthContext (warunkowe wyświetlanie opcji):

1function Navigation() {
2  const { user, logout } = useAuth();
3
4  return (
5    <nav>
6      <NavLink to="/" className={({isActive}) => isActive ? 'active' : ''}>
7        Home
8      </NavLink>
9      <NavLink to="/missions" className={({isActive}) => isActive ? 'active' : ''}>
10        Missions
11      </NavLink>
12
13      {user ? (
14        <>
15          <NavLink to="/dashboard" className={({isActive}) => isActive ? 'active' : ''}>
16            Dashboard
17          </NavLink>
18          <span>Zalogowany: {user.name}</span>
19          <button onClick={logout}>Wyloguj</button>
20        </>
21      ) : (
22        <NavLink to="/login">Logowanie</NavLink>
23      )}
24    </nav>
25  );
26}

Dzięki temu nawigacja automatycznie reaguje na stan uwierzytelniania - zalogowany użytkownik widzi Dashboard i przycisk wylogowania, niezalogowany widzi link do logowania.

Zagnieżdżone trasy z layoutem

Sekcja misji wykorzystuje zagnieżdżone trasy z Outlet:

1function MissionsLayout() {
2  return (
3    <div className="missions-layout">
4      <aside>
5        <h3>Misje</h3>
6        <NavLink to="/missions/apollo">Apollo</NavLink>
7        <NavLink to="/missions/artemis">Artemis</NavLink>
8        <NavLink to="/missions/voyager">Voyager</NavLink>
9      </aside>
10      <main>
11        <Outlet />
12      </main>
13    </div>
14  );
15}
16
17// W konfiguracji tras:
18<Route path="/missions" element={<MissionsLayout />}>
19  <Route index element={<MissionsList />} />
20  <Route path=":missionId" element={<MissionDetail />} />
21</Route>

Index route (<Route index />) renderuje domyślną zawartość (listę misji), gdy użytkownik jest na /missions. Dynamiczny parametr :missionId obsługuje szczegóły konkretnej misji.

Kompletna konfiguracja tras

Oto jak wygląda pełna konfiguracja tras łącząca wszystkie poznane techniki:

1function App() {
2  return (
3    <AuthProvider>
4      <BrowserRouter>
5        <Navigation />
6        <Routes>
7          {/* Strona główna */}
8          <Route path="/" element={<HomePage />} />
9
10          {/* Logowanie */}
11          <Route path="/login" element={<LoginPage />} />
12
13          {/* Zagnieżdżone trasy z layoutem */}
14          <Route path="/missions" element={<MissionsLayout />}>
15            <Route index element={<MissionsList />} />
16            <Route path=":missionId" element={<MissionDetail />} />
17          </Route>
18
19          {/* Chroniona trasa */}
20          <Route path="/dashboard" element={
21            <ProtectedRoute>
22              <DashboardPage />
23            </ProtectedRoute>
24          } />
25
26          {/* Obsługa 404 */}
27          <Route path="*" element={<NotFound />} />
28        </Routes>
29      </BrowserRouter>
30    </AuthProvider>
31  );
32}

Kolejność elementów ma znaczenie:

  • Konkretne ścieżki (/, /login, /missions) są dopasowywane jako pierwsze
  • Trasa path="*" musi być ostatnia - przechwytuje wszystko, co nie pasuje do żadnej zdefiniowanej trasy

Logowanie z zapamiętaniem ścieżki

Strona logowania wykorzystuje useNavigate i useLocation do przekierowania użytkownika z powrotem na stronę, z której został przekierowany:

1function LoginPage() {
2  const [name, setName] = useState('');
3  const { login } = useAuth();
4  const navigate = useNavigate();
5  const location = useLocation();
6
7  // Ścieżka, z której użytkownik został przekierowany
8  const from = location.state?.from?.pathname || '/dashboard';
9
10  const handleSubmit = (e) => {
11    e.preventDefault();
12    if (name.trim()) {
13      login(name);
14      navigate(from, { replace: true }); // replace: true usuwa /login z historii
15    }
16  };
17
18  return (
19    <form onSubmit={handleSubmit}>
20      <input value={name} onChange={(e) => setName(e.target.value)} />
21      <button type="submit">Zaloguj</button>
22    </form>
23  );
24}

Opcja replace: true w navigate jest kluczowa - dzięki niej użytkownik po zalogowaniu nie wróci na stronę logowania po kliknięciu "Wstecz" w przeglądarce.

Obsługa strony 404

Strona Not Found zawiera link powrotny do strony głównej:

1function NotFound() {
2  return (
3    <div className="not-found">
4      <h1>404</h1>
5      <p>Strona nie została odnaleziona</p>
6      <Link to="/">Wróć na stronę główną</Link>
7    </div>
8  );
9}

Podsumowanie architektury

Kompletna aplikacja z routingiem w React łączy następujące elementy:

ElementRolaHook / Komponent
Kontener trasWłącza routingBrowserRouter
Definicja trasMapuje URL na komponentyRoutes, Route
NawigacjaLinki z aktywnymi stylamiNavLink
ZagnieżdżanieWspółdzielony layoutOutlet, Route children
Parametry dynamiczneSzczegóły elementówuseParams
Nawigacja programowaPrzekierowanie po akcjiuseNavigate
Stan lokalizacjiPrzekazywanie ukrytych danychuseLocation
Ochrona trasWymuszanie logowaniaProtectedRoute + Navigate
Obsługa błędówStrony nieistniejąceRoute path="*"
Kontekst authGlobalne dane użytkownikacreateContext + useContext

Wypróbuj kompletną aplikację w edytorze poniżej - zaloguj się, przejdź między misjami, sprawdź Dashboard i przetestuj stronę 404!

Kod do tej lekcji: App.jsx
1import React, { useState, createContext, useContext, Suspense, lazy } from 'react';
2import {
3  BrowserRouter, Routes, Route, Link, NavLink,
4  useParams, useNavigate, useLocation, Navigate, Outlet
5} from 'react-router-dom';
6
7// Auth Context
8const AuthContext = createContext();
9function useAuth() { return useContext(AuthContext); }
10
11function AuthProvider({ children }) {
12  const [user, setUser] = useState(null);
13  const login = (name) => setUser({ name });
14  const logout = () => setUser(null);
15  return (
16    <AuthContext.Provider value={{ user, login, logout }}>
17      {children}
18    </AuthContext.Provider>
19  );
20}
21
22// Protected Route
23function ProtectedRoute({ children }) {
24  const { user } = useAuth();
25  const location = useLocation();
26  if (!user) return <Navigate to="/login" state={{ from: location }} replace />;
27  return children;
28}
29
30// Components
31function Navigation() {
32  const { user, logout } = useAuth();
33  return (
34    <nav>
35      <div className="nav-brand">SpaceApp</div>
36      <div className="nav-links">
37        <NavLink to="/" className={({isActive}) => isActive ? 'active' : ''}>Home</NavLink>
38        <NavLink to="/missions" className={({isActive}) => isActive ? 'active' : ''}>Missions</NavLink>
39        {user ? (
40          <>
41            <NavLink to="/dashboard" className={({isActive}) => isActive ? 'active' : ''}>Dashboard</NavLink>
42            <span className="user-badge">{user.name}</span>
43            <button onClick={logout}>Logout</button>
44          </>
45        ) : (
46          <NavLink to="/login" className={({isActive}) => isActive ? 'active' : ''}>Login</NavLink>
47        )}
48      </div>
49    </nav>
50  );
51}
52
53function HomePage() {
54  return (
55    <div className="page home">
56      <h1>Welcome to SpaceApp</h1>
57      <p>Your gateway to space exploration</p>
58      <div className="features">
59        <div className="feature">Track Missions</div>
60        <div className="feature">View Analytics</div>
61        <div className="feature">Manage Crew</div>
62      </div>
63    </div>
64  );
65}
66
67function LoginPage() {
68  const [name, setName] = useState('');
69  const { login } = useAuth();
70  const navigate = useNavigate();
71  const location = useLocation();
72  const from = location.state?.from?.pathname || '/dashboard';
73
74  const handleSubmit = (e) => {
75    e.preventDefault();
76    if (name.trim()) {
77      login(name);
78      navigate(from, { replace: true });
79    }
80  };
81
82  return (
83    <div className="page login">
84      <h1>Login</h1>
85      <form onSubmit={handleSubmit}>
86        <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Username" />
87        <button type="submit">Login</button>
88      </form>
89    </div>
90  );
91}
92
93function MissionsLayout() {
94  return (
95    <div className="missions-layout">
96      <aside>
97        <h3>Missions</h3>
98        <NavLink to="/missions/apollo">Apollo</NavLink>
99        <NavLink to="/missions/artemis">Artemis</NavLink>
100        <NavLink to="/missions/voyager">Voyager</NavLink>
101      </aside>
102      <main><Outlet /></main>
103    </div>
104  );
105}
106
107function MissionsList() {
108  return <div className="content"><h2>Select a mission</h2></div>;
109}
110
111function MissionDetail() {
112  const { missionId } = useParams();
113  return (
114    <div className="content">
115      <h2>Mission: {missionId?.toUpperCase()}</h2>
116      <p>Details for {missionId} mission</p>
117    </div>
118  );
119}
120
121function DashboardPage() {
122  const { user } = useAuth();
123  return (
124    <div className="page dashboard">
125      <h1>Dashboard</h1>
126      <p>Welcome back, {user?.name}!</p>
127      <div className="stats">
128        <div className="stat"><span>Missions</span>12</div>
129        <div className="stat"><span>Crew</span>48</div>
130        <div className="stat"><span>Ships</span>8</div>
131      </div>
132    </div>
133  );
134}
135
136function NotFound() {
137  return (
138    <div className="page not-found">
139      <h1>404</h1>
140      <p>Page not found</p>
141      <Link to="/">Go Home</Link>
142    </div>
143  );
144}
145
146function App() {
147  return (
148    <AuthProvider>
149      <BrowserRouter>
150        <div className="app">
151          <Navigation />
152          <Routes>
153            <Route path="/" element={<HomePage />} />
154            <Route path="/login" element={<LoginPage />} />
155            <Route path="/missions" element={<MissionsLayout />}>
156              <Route index element={<MissionsList />} />
157              <Route path=":missionId" element={<MissionDetail />} />
158            </Route>
159            <Route path="/dashboard" element={
160              <ProtectedRoute><DashboardPage /></ProtectedRoute>
161            } />
162            <Route path="*" element={<NotFound />} />
163          </Routes>
164        </div>
165      </BrowserRouter>
166    </AuthProvider>
167  );
168}
169
170export 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. 1. Jak zaimplementować Protected Route w React Router?

  2. 2. Gdzie w komponencie nadrzędnym umieszczamy <Outlet />, aby renderować zagnieżdżone trasy?

To 2 z 7 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Zaimplementuj obsługę błędów 404 z Route path='*' i komponentem NotFound. Dodaj nawigację z linkami do istniejących i nieistniejących stron, aby przetestować obsługę błędów.

  • Klikanie w kolejności

    Uporządkuj programową nawigację z state:

  • Układanie w pionie

    Uporządkuj kroki programatycznej nawigacji za pomocą useNavigate:

  • Układanie w pionie

    Uporządkuj kroki przekazywania i odczytywania danych między trasami:

  • Układanie w pionie

    Uporządkuj kroki implementacji Protected Route z komponentem Navigate:

  • Klikanie w kolejności

    Uporządkuj zarządzanie search params:

  • Układanie w pionie

    Uporządkuj kroki nawigacji w React Router od kliknięcia linku do wyświetlenia strony:

  • Układanie w pionie

    Uporządkuj kroki obsługi nieautoryzowanego dostępu do chronionej trasy:

  • Układanie w poziomie

    Ułóż elementy trasy obsługującej stronę 404:

  • Układanie w poziomie

    Ułóż składnię Route z dynamicznym parametrem id:

  • Edytor kodu

    Zbuduj kompleksowy Navigation System zawierający: React Router setup z BrowserRouter, nested routes dla struktury /fleet/:id/missions/:missionId, dynamic routing z params, Link i NavLink components z active styles, useNavigate dla programmatic navigation, useParams i useLocation hooks, protected routes z authentication, lazy loading z React.lazy i Suspense, search params z useSearchParams, oraz 404 error handling. Stwórz system nawigacyjny godny najbardziej zaawansowanej floty przemierzającej Pas Asteroid!

Przydatne artykuły