Lekcja 19: React Router - Podstawy Routingu

Większość aplikacji webowych składa się z wielu "stron" lub widoków. W tradycyjnych aplikacjach wielostronicowych (MPA - Multi-Page Applications), nawigacja między stronami powoduje pełne przeładowanie strony przez przeglądarkę. W aplikacjach jednostronicowych (SPA - Single Page Applications), takich jak te budowane w React, chcemy zapewnić płynną nawigację między widokami bez przeładowywania strony. Do tego służy routing po stronie klienta (client-side routing), a najpopularniejszą biblioteką do jego implementacji w React jest React Router.

Czym jest React Router?

React Router to biblioteka, która synchronizuje interfejs użytkownika aplikacji React z adresem URL w przeglądarce. Pozwala definiować różne widoki (komponenty) dla różnych ścieżek URL i zarządzać nawigacją między nimi bez konieczności odświeżania całej strony.

Instalacja

Do aplikacji webowych React instalujemy pakiet react-router-dom:

npm install react-router-dom

Lub używając yarn:

yarn add react-router-dom

Podstawowe Komponenty React Router (v6)

React Router v6 wprowadził kilka zmian w porównaniu do poprzednich wersji. Kluczowe komponenty to:

Podstawowa Konfiguracja Routingu

// src/index.js lub src/main.jsx (zależnie od konfiguracji projektu)
import React from \"react\";
import ReactDOM from \"react-dom/client\";
import { BrowserRouter } from \"react-router-dom\";
import App from \"./App\";

const root = ReactDOM.createRoot(document.getElementById(\"root\"));
root.render(
  <React.StrictMode>
    <BrowserRouter> { /* Owiń App w BrowserRouter */ }
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

// src/App.jsx
import React from \"react\";
import { Routes, Route, Link, NavLink } from \"react-router-dom\";

// Przykładowe komponenty stron
function Home() { return <h2>Strona Główna</h2>; }
function About() { return <h2>O Nas</h2>; }
function Contact() { return <h2>Kontakt</h2>; }

function App() {
  const navLinkStyles = ({ isActive }) => {
      return {
          fontWeight: isActive ? \"bold\" : \"normal\",
          textDecoration: isActive ? \"underline\" : \"none\",
          marginRight: \"10px\"
      };
  };

  return (
    <div>
      <nav>
        <NavLink to=\"/\" style={navLinkStyles}>Home</NavLink>
        <NavLink to=\"/about\" style={navLinkStyles}>O Nas</NavLink>
        <NavLink to=\"/contact\" style={navLinkStyles}>Kontakt</NavLink>
      </nav>

      <hr />

      {/* Definicja tras */}
      <Routes>
        <Route path=\"/\" element={<Home />} />
        <Route path=\"/about\" element={<About />} />
        <Route path=\"/contact\" element={<Contact />} />
      </Routes>
    </div>
  );
}

export default App;

Dynamiczne Trasy i Parametry URL (`useParams`)

Często potrzebujemy tras, które zawierają dynamiczne segmenty, np. ID produktu czy nazwę użytkownika. Definiujemy je za pomocą dwukropka (:) w atrybucie path.

Aby uzyskać dostęp do wartości tych parametrów w komponencie renderowanym przez trasę, używamy Hooka useParams.

import { useParams } from \"react-router-dom\";

function UserProfile() {
  // useParams zwraca obiekt z parametrami URL
  const { userId } = useParams(); 
  // Nazwa właściwości (userId) musi pasować do nazwy parametru w path (\":userId\")

  return <h2>Profil użytkownika o ID: {userId}</h2>;
}

// W definicji tras w App.jsx:
// <Route path=\"/users/:userId\" element={<UserProfile />} />

// Link do takiej trasy:
// <Link to=\"/users/123\">Profil użytkownika 123</Link>

Trasy Zagnieżdżone (`<Outlet />`)

React Router pozwala na tworzenie zagnieżdżonych struktur tras, co jest przydatne przy budowaniu layoutów z częściami wspólnymi (np. panel boczny) i częściami zmieniającymi się.

Komponent nadrzędnej trasy renderuje komponent <Outlet />, który działa jak placeholder - w tym miejscu zostanie wyrenderowany komponent pasującej trasy podrzędnej.

import { Outlet, Link } from \"react-router-dom\";

// Komponent layoutu dla sekcji /dashboard
function DashboardLayout() {
  return (
    <div style={{ display: \"flex\" }}>
      <nav style={{ borderRight: \"1px solid #ccc\", padding: \"10px\", width: \"150px\" }}>
        <h3>Dashboard</h3>
        <ul style={{ listStyle: \"none\", padding: 0 }}>
          <li><Link to=\"/dashboard\">Przegląd</Link></li>
          <li><Link to=\"/dashboard/settings\">Ustawienia</Link></li>
        </ul>
      </nav>
      <main style={{ padding: \"10px\", flexGrow: 1 }}>
        {/* Tutaj renderują się komponenty tras podrzędnych */}
        <Outlet />
      </main>
    </div>
  );
}

function DashboardOverview() { return <h4>Przegląd Dashboardu</h4>; }
function DashboardSettings() { return <h4>Ustawienia Dashboardu</h4>; }

// W definicji tras w App.jsx:
// <Route path=\"/dashboard\" element={<DashboardLayout />}>
//   {/* Trasy podrzędne - ścieżki są względne do rodzica */}
//   <Route index element={<DashboardOverview />} /> { /* index oznacza domyślną trasę dla /dashboard */}
//   <Route path=\"settings\" element={<DashboardSettings />} /> { /* Pełna ścieżka: /dashboard/settings */}
// </Route>

Nawigacja Programatyczna (`useNavigate`)

Czasami potrzebujemy przekierować użytkownika do innej trasy w odpowiedzi na jakieś zdarzenie (np. po zalogowaniu, po wysłaniu formularza), a nie tylko po kliknięciu linku. Do tego służy Hook useNavigate.

import { useNavigate } from \"react-router-dom\";

function LoginForm() {
  const navigate = useNavigate();

  const handleLogin = () => {
    // Symulacja logowania...
    console.log(\"Zalogowano!\");
    // Przekierowanie do dashboardu po zalogowaniu
    navigate(\"/dashboard\"); 
    // Można też użyć navigate(-1) do cofnięcia się o jedną stronę w historii
    // lub navigate(\"/profile\", { replace: true }) do zastąpienia bieżącej strony w historii
  };

  return (
    <div>
      <h3>Logowanie</h3>
      {/* ... pola formularza ... */}
      <button onClick={handleLogin}>Zaloguj</button>
    </div>
  );
}

Brak Dopasowania (No Match / 404)

Aby obsłużyć sytuację, gdy żadna z zdefiniowanych tras nie pasuje do aktualnego URL, możemy dodać trasę z path="*" na końcu definicji <Routes>.

function NotFound() { return <h2>404 - Strona nie znaleziona</h2>; }

// W definicji tras w App.jsx:
// <Routes>
//   <Route path=\"/\" element={<Home />} />
//   <Route path=\"/about\" element={<About />} />
//   {/* ... inne trasy ... */}
//   <Route path=\"*\" element={<NotFound />} /> { /* Ta trasa pasuje do wszystkiego innego */}
// </Routes>

Ćwiczenie praktyczne

Pokaż rozwiązanie
// src/index.js lub src/main.jsx
import React from \"react\";
import ReactDOM from \"react-dom/client\";
import { BrowserRouter } from \"react-router-dom\";
import App from \"./App\";

const root = ReactDOM.createRoot(document.getElementById(\"root\"));
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

// src/App.jsx
import React from \"react\";
import { Routes, Route, Link, NavLink, useParams } from \"react-router-dom\";

// Komponenty stron
function Home() { return <h2>Witaj na stronie głównej!</h2>; }

function Products() {
  return (
    <div>
      <h2>Produkty</h2>
      <ul>
        <li><Link to=\"/products/1\">Produkt 1</Link></li>
        <li><Link to=\"/products/2\">Produkt 2</Link></li>
        <li><Link to=\"/products/abc\">Produkt ABC</Link></li>
      </ul>
    </div>
  );
}

function ProductDetail() {
  const { productId } = useParams();
  return <h2>Szczegóły produktu: {productId}</h2>;
}

function NotFound() { return <h2>404 - Nie znaleziono strony</h2>; }

// Główny komponent aplikacji
function App() {
  const navLinkStyles = ({ isActive }) => ({
    fontWeight: isActive ? \"bold\" : \"normal\",
    marginRight: \"10px\"
  });

  return (
    <div>
      <nav style={{ padding: \"10px\", background: \"#eee\" }}>
        <NavLink to=\"/\" style={navLinkStyles}>Home</NavLink>
        <NavLink to=\"/products\" style={navLinkStyles}>Produkty</NavLink>
      </nav>
      <hr />
      <div style={{ padding: \"10px\" }}>
        <Routes>
          <Route path=\"/\" element={<Home />} />
          <Route path=\"/products\" element={<Products />} />
          <Route path=\"/products/:productId\" element={<ProductDetail />} />
          <Route path=\"*\" element={<NotFound />} />
        </Routes>
      </div>
    </div>
  );
}

export default App;

Cel: Stworzyć prostą aplikację z trzema stronami (Home, Products, ProductDetail) używając React Router.

Kroki:

  1. Zainstaluj react-router-dom.
  2. Owiń komponent App w <BrowserRouter> w pliku index.js/main.jsx.
  3. Stwórz komponenty dla stron: Home, Products, ProductDetail.
  4. W komponencie App stwórz nawigację (użyj <NavLink>) do strony Home i Products.
  5. W komponencie App zdefiniuj trasy za pomocą <Routes> i <Route>:
  6. W komponencie Products wyświetl listę linków (<Link>) do kilku przykładowych produktów (np. /products/1, /products/2).
  7. W komponencie ProductDetail użyj Hooka useParams, aby odczytać productId z URL i wyświetlić go.
  8. Dodaj trasę "catch-all" (path="*") renderującą prosty komponent NotFound.

Zadanie do samodzielnego wykonania

Rozbuduj aplikację z ćwiczenia praktycznego:

  1. Dodaj nową sekcję "Admin" z zagnieżdżonymi trasami.
  2. Stwórz komponent AdminLayout z własną nawigacją (np. "Użytkownicy", "Ustawienia") i komponentem <Outlet />.
  3. Zdefiniuj trasę /admin, która renderuje AdminLayout.
  4. Dodaj trasy podrzędne wewnątrz /admin, np. /admin/users i /admin/settings, które renderują odpowiednie komponenty wewnątrz <Outlet /> w AdminLayout.
  5. Dodaj link do sekcji Admin w głównej nawigacji.

FAQ - React Router - Podstawy Routingu

Jaka jest różnica między `BrowserRouter` a `HashRouter`?

`BrowserRouter` używa HTML5 History API do tworzenia "czystych" URL-i (np. `/users/123`). Wymaga konfiguracji po stronie serwera, aby poprawnie obsługiwać odświeżanie strony na dowolnej ścieżce. `HashRouter` używa hasha w URL (np. `/#/users/123`). Nie wymaga specjalnej konfiguracji serwera, ale URL-e są mniej estetyczne i mogą mieć problemy z SEO. `BrowserRouter` jest zazwyczaj preferowany.

Czy mogę używać zwykłych tagów `<a>` zamiast `<Link>`?

Użycie zwykłego tagu `<a href="...">` spowoduje pełne przeładowanie strony przez przeglądarkę, co niweczy ideę SPA i routingu po stronie klienta. Należy używać komponentu `<Link>` (lub `<NavLink>`), aby React Router mógł przechwycić nawigację i zaktualizować UI bez przeładowania.

Jak przekazać dodatkowe dane (stan) podczas nawigacji?

Komponent `Link` oraz funkcja `navigate` pozwalają na przekazanie stanu za pomocą opcji `state`. Dane te są dostępne w komponencie docelowym za pomocą Hooka `useLocation`. Przykład: `<Link to="/profile" state={{ from: location }} />` lub `navigate("/profile", { state: { userId: 123 } })`. W komponencie docelowym: `const location = useLocation(); const userId = location.state?.userId;`.

Jak ostylować aktywny link za pomocą `NavLink`?

`NavLink` pozwala przekazać funkcję do propa `style` lub `className`. Funkcja ta otrzymuje obiekt z właściwością `isActive` (boolean). Można zwrócić odpowiedni obiekt stylu lub nazwę klasy w zależności od wartości `isActive`. Przykład: `className={({ isActive }) => isActive ? "active-link" : "inactive-link"}`.

Czy mogę mieć wiele zestawów `<Routes>` w aplikacji?

Tak, można używać `<Routes>` w różnych miejscach aplikacji, np. do definiowania tras głównych i zagnieżdżonych tras w layoutach. Każdy `<Routes>` będzie renderował tylko pierwszy pasujący `<Route>` wewnątrz siebie.

Jak obsługiwać parametry zapytania (query parameters) w URL (np. `?sort=asc`)?

React Router v6 udostępnia Hook `useSearchParams` do odczytywania i modyfikowania parametrów zapytania w URL. Zwraca on obiekt `URLSearchParams` i funkcję do jego aktualizacji. Przykład: `const [searchParams, setSearchParams] = useSearchParams(); const sortOrder = searchParams.get("sort"); setSearchParams({ sort: "desc" });`.

Co to jest `Outlet`?

`Outlet` to komponent używany w komponentach tras nadrzędnych (layoutach) do wskazania miejsca, w którym powinny być renderowane komponenty pasujących tras podrzędnych. Działa jak placeholder dla zagnieżdżonej zawartości trasy.