Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-02-03
Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-02-03
Udostępnij Udostępnij Kontakt
Wprowadzenie

Dotychczas wszystkie nasze aplikacje składały się z jednego "widoku" – wszystkie komponenty renderowały się jednocześnie. Ale prawdziwe aplikacje webowe składają się z wielu stron: strona główna, profil użytkownika, ustawienia, produkty, szczegóły produktu, dashboard. Użytkownicy oczekują możliwości nawigacji między tymi stronami bez przeładowywania całej aplikacji.

W świecie Single Page Applications potrzebujemy client-side routing – mechanizmu, który zmienia zawartość strony w odpowiedzi na zmianę URL, ale bez wysyłania request do serwera. Do tego służy React Router – najpopularniejsza biblioteka do routingu w React.

W tym wpisie nauczymy się instalować i konfigurować React Router, tworzyć trasy (routes), linkować między nimi, przekazywać parametry przez URL, tworzyć chronione trasy (protected routes) wymagające autoryzacji, oraz poznamy wszystkie hooki do pracy z routingiem. To będzie praktyczny wpis – po nim Twoja aplikacja zamieni się w pełnoprawną aplikację wielostronicową!

Czym jest React Router?

React Router to biblioteka, która:

  • Synchronizuje UI z URL
  • Pozwala na deklaratywne definiowanie tras
  • Zapewnia nawigację bez przeładowania strony
  • Dostarcza hooki do pracy z historią przeglądarki, parametrami URL, itp.

Historia wersji:

  • React Router v5 – stara wersja (jeszcze spotykana w legacy projektach)
  • React Router v6 – aktualna wersja (której będziemy się uczyć)

Wersja 6 jest prostsza, bardziej intuicyjna i ma lepsze wsparcie dla TypeScript.

Instalacja React Router

npm install react-router-dom

To wszystko! react-router-dom zawiera wszystko czego potrzebujemy dla aplikacji webowych.

Podstawowa konfiguracja

Krok 1: Owinięcie aplikacji w BrowserRouter

// main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './index.css';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

BrowserRouter używa History API przeglądarki do zarządzania URL.

Krok 2: Definiowanie tras

// App.tsx
import { Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
import NotFound from './pages/NotFound';

function App() {
  return (
    <div className="app">
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </div>
  );
}

export default App;

Co tu się dzieje?

  • <Routes> – kontener dla wszystkich tras
  • <Route> – definiuje pojedynczą trasę
  • path – ścieżka URL
  • element – komponent do wyrenderowania
  • path="*" – catch-all route (404)

Krok 3: Komponenty stron

// pages/Home.tsx
function Home() {
  return (
    <div>
      <h1>Strona główna</h1>
      <p>Witaj w naszej aplikacji!</p>
    </div>
  );
}

export default Home;

// pages/About.tsx
function About() {
  return (
    <div>
      <h1>O nas</h1>
      <p>Jesteśmy firmą...</p>
    </div>
  );
}

export default About;

// pages/NotFound.tsx
function NotFound() {
  return (
    <div>
      <h1>404 - Strona nie znaleziona</h1>
      <p>Przepraszamy, ta strona nie istnieje.</p>
    </div>
  );
}

export default NotFound;
Nawigacja – Link i NavLink

Link – podstawowa nawigacja

import { Link } from 'react-router-dom';

function Navigation() {
  return (
    <nav>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
      <Link to="/contact">Contact</Link>
    </nav>
  );
}

<Link> renderuje <a> ale nie przeładowuje strony – nawigacja odbywa się przez JavaScript!

Nigdy nie używaj <a href="..."> w React Router! To spowoduje pełne przeładowanie aplikacji.

NavLink – link z aktywnym stanem

import { NavLink } from 'react-router-dom';

function Navigation() {
  return (
    <nav>
      <NavLink 
        to="/" 
        className={({ isActive }) => isActive ? 'active' : ''}
      >
        Home
      </NavLink>
      <NavLink 
        to="/about"
        className={({ isActive }) => isActive ? 'active' : ''}
      >
        About
      </NavLink>
      <NavLink 
        to="/contact"
        className={({ isActive }) => isActive ? 'active' : ''}
      >
        Contact
      </NavLink>
    </nav>
  );
}

NavLink wie która trasa jest aktywna i może stylować się inaczej!

Style CSS:

nav a {
  margin: 0 10px;
  text-decoration: none;
  color: #333;
}

nav a.active {
  color: #3498db;
  font-weight: bold;
  border-bottom: 2px solid #3498db;
}
Parametry URL

Dynamic Segments (parametry w ścieżce)

// App.tsx
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/users/:userId" element={<UserProfile />} />
  <Route path="/products/:productId" element={<ProductDetails />} />
</Routes>

:userId to parametr – może być dowolna wartość.

Odczytywanie parametrów – useParams

// pages/UserProfile.tsx
import { useParams } from 'react-router-dom';
import { useState, useEffect } from 'react';

interface User {
  id: number;
  name: string;
  email: string;
}

function UserProfile() {
  const { userId } = useParams<{ userId: string }>();
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchUser() {
      try {
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();
        setUser(data);
      } catch (error) {
        console.error('Error fetching user:', error);
      } finally {
        setLoading(false);
      }
    }

    fetchUser();
  }, [userId]);

  if (loading) return <div>Ładowanie...</div>;
  if (!user) return <div>Użytkownik nie znaleziony</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
      <p>ID: {user.id}</p>
    </div>
  );
}

export default UserProfile;

TypeScript tip: useParams<{ userId: string }>() typuje parametry!

Linkowanie z parametrami

// UserList.tsx
import { Link } from 'react-router-dom';

function UserList() {
  const users = [
    { id: 1, name: 'Paweł' },
    { id: 2, name: 'Anna' },
    { id: 3, name: 'Jan' }
  ];

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          <Link to={`/users/${user.id}`}>{user.name}</Link>
        </li>
      ))}
    </ul>
  );
}
Query Parameters (parametry zapytania)

URL: /search?q=react&category=tutorials&sort=date

Odczytywanie query params – useSearchParams

import { useSearchParams } from 'react-router-dom';

function SearchPage() {
  const [searchParams, setSearchParams] = useSearchParams();

  const query = searchParams.get('q') || '';
  const category = searchParams.get('category') || 'all';
  const sort = searchParams.get('sort') || 'relevance';

  function handleQueryChange(newQuery: string) {
    setSearchParams({ q: newQuery, category, sort });
  }

  function handleCategoryChange(newCategory: string) {
    setSearchParams({ q: query, category: newCategory, sort });
  }

  return (
    <div>
      <h1>Wyszukiwanie</h1>
      <input
        type="text"
        value={query}
        onChange={(e) => handleQueryChange(e.target.value)}
        placeholder="Szukaj..."
      />
      <select value={category} onChange={(e) => handleCategoryChange(e.target.value)}>
        <option value="all">Wszystkie</option>
        <option value="tutorials">Tutoriale</option>
        <option value="articles">Artykuły</option>
      </select>
      <p>Szukasz: "{query}" w kategorii: {category}</p>
    </div>
  );
}

setSearchParams aktualizuje URL bez przeładowania strony!

Programowa nawigacja – useNavigate

Czasem potrzebujesz nawigować programowo (po submit formularza, po loginie, itp.):

import { useNavigate } from 'react-router-dom';
import { useState } from 'react';

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const navigate = useNavigate();

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();

    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password })
      });

      if (response.ok) {
        // Nawiguj do dashboard po udanym logowaniu
        navigate('/dashboard');
      }
    } catch (error) {
      console.error('Login failed:', error);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Hasło"
      />
      <button type="submit">Zaloguj</button>
    </form>
  );
}

Opcje navigate

// Nawiguj forward
navigate('/dashboard');

// Nawiguj backward (replace history entry)
navigate('/dashboard', { replace: true });

// Nawiguj do poprzedniej strony
navigate(-1);

// Nawiguj do następnej strony (jeśli użytkownik cofnął się)
navigate(1);

// Nawiguj z state
navigate('/dashboard', { state: { from: 'login' } });
Zagnieżdżone trasy (Nested Routes)

Dla złożonych aplikacji możesz zagnieżdżać trasy:

// App.tsx
import { Routes, Route } from 'react-router-dom';
import Dashboard from './pages/Dashboard';
import Profile from './pages/dashboard/Profile';
import Settings from './pages/dashboard/Settings';
import Analytics from './pages/dashboard/Analytics';

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/dashboard" element={<Dashboard />}>
        <Route path="profile" element={<Profile />} />
        <Route path="settings" element={<Settings />} />
        <Route path="analytics" element={<Analytics />} />
      </Route>
    </Routes>
  );
}

Outlet – miejsce na nested routes

// pages/Dashboard.tsx
import { Outlet, Link } from 'react-router-dom';

function Dashboard() {
  return (
    <div className="dashboard">
      <aside>
        <h2>Dashboard Menu</h2>
        <nav>
          <Link to="/dashboard/profile">Profil</Link>
          <Link to="/dashboard/settings">Ustawienia</Link>
          <Link to="/dashboard/analytics">Analityka</Link>
        </nav>
      </aside>
      <main>
        {/* Tutaj renderują się zagnieżdżone trasy */}
        <Outlet />
      </main>
    </div>
  );
}

export default Dashboard;

URL i komponenty:

  • /dashboard → Dashboard (tylko sidebar)
  • /dashboard/profile → Dashboard + Profile
  • /dashboard/settings → Dashboard + Settings
Protected Routes (chronione trasy)

Często potrzebujesz tras dostępnych tylko dla zalogowanych użytkowników:

// components/ProtectedRoute.tsx
import { Navigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';

interface ProtectedRouteProps {
  children: React.ReactNode;
}

function ProtectedRoute({ children }: ProtectedRouteProps) {
  // tutaj dostaniecie błąd, że nie można destrukturyzować pola /loading
  // wiecie dlaczego, prawda ?
  // jeżeli nie, sprawdzcie implementacje swojego hooka useAuth - wszystko stanie się jasne :)
  const { isAuthenticated, loading } = useAuth();

  if (loading) {
    return <div>Sprawdzanie autoryzacji...</div>;
  }

  if (!isAuthenticated) {
    // Przekieruj do logowania
    return <Navigate to="/login" replace />;
  }

  return <>{children}</>;
}

export default ProtectedRoute;

Użycie:

// App.tsx
import ProtectedRoute from './components/ProtectedRoute';

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/login" element={<Login />} />
      
      {/* Chroniona trasa */}
      <Route
        path="/dashboard"
        element={
          <ProtectedRoute>
            <Dashboard />
          </ProtectedRoute>
        }
      />
      
      {/* Zagnieżdżone chronione trasy */}
      <Route
        path="/profile"
        element={
          <ProtectedRoute>
            <Profile />
          </ProtectedRoute>
        }
      />
    </Routes>
  );
}

Zapisywanie poprzedniej lokalizacji

Często chcesz przekierować użytkownika z powrotem do strony, którą próbował odwiedzić:

// ProtectedRoute.tsx
import { Navigate, useLocation } from 'react-router-dom';

function ProtectedRoute({ children }: ProtectedRouteProps) {
  const { isAuthenticated, loading } = useAuth();
  const location = useLocation();

  if (loading) {
    return <div>Sprawdzanie autoryzacji...</div>;
  }

  if (!isAuthenticated) {
    // Zapisz lokalizację w state
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return <>{children}</>;
}
// Login.tsx
import { useNavigate, useLocation } from 'react-router-dom';

function Login() {
  const navigate = useNavigate();
  const location = useLocation();

  async function handleLogin() {
    await loginUser();
    
    // Pobierz poprzednią lokalizację lub idź do dashboard
    const from = (location.state as any)?.from?.pathname || '/dashboard';
    navigate(from, { replace: true });
  }

  return (
    // formularz logowania
  );
}
useLocation Hook

import { useLocation } from 'react-router-dom';

function CurrentPath() {
  const location = useLocation();

  return (
    <div>
      <p>Pathname: {location.pathname}</p>
      <p>Search: {location.search}</p>
      <p>Hash: {location.hash}</p>
      <p>State: {JSON.stringify(location.state)}</p>
    </div>
  );
}

location zawiera:

  • pathname/users/123
  • search?q=react&sort=date
  • hash#section-2
  • state – dane przekazane przez navigate
Layout Routes

Dla wspólnego layoutu (header, footer) dla wielu tras:

// layouts/MainLayout.tsx
import { Outlet } from 'react-router-dom';
import Header from '../components/Header';
import Footer from '../components/Footer';

function MainLayout() {
  return (
    <div className="main-layout">
      <Header />
      <main>
        <Outlet />
      </main>
      <Footer />
    </div>
  );
}

export default MainLayout;
// App.tsx
function App() {
  return (
    <Routes>
      {/* Trasy z layout */}
      <Route element={<MainLayout />}>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
      </Route>

      {/* Trasy bez layout */}
      <Route path="/login" element={<Login />} />
      <Route path="/register" element={<Register />} />
    </Routes>
  );
}
Index Routes

Index route renderuje się gdy parent route ma dokładnie ten URL:

<Routes>
  <Route path="/dashboard" element={<Dashboard />}>
    <Route index element={<DashboardHome />} />
    <Route path="profile" element={<Profile />} />
    <Route path="settings" element={<Settings />} />
  </Route>
</Routes>
  • /dashboard → Dashboard + DashboardHome
  • /dashboard/profile → Dashboard + Profile
Kompletny przykład: Blog App

Połączmy wszystko w praktyczną aplikację:

// App.tsx
import { Routes, Route } from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import Home from './pages/Home';
import Blog from './pages/Blog';
import PostDetails from './pages/PostDetails';
import CreatePost from './pages/CreatePost';
import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
import NotFound from './pages/NotFound';
import ProtectedRoute from './components/ProtectedRoute';

function App() {
  return (
    <Routes>
      {/* Publiczne trasy z layout */}
      <Route element={<MainLayout />}>
        <Route path="/" element={<Home />} />
        <Route path="/blog" element={<Blog />} />
        <Route path="/blog/:postId" element={<PostDetails />} />
      </Route>

      {/* Trasy autoryzacji bez layout */}
      <Route path="/login" element={<Login />} />

      {/* Chronione trasy */}
      <Route
        path="/dashboard"
        element={
          <ProtectedRoute>
            <Dashboard />
          </ProtectedRoute>
        }
      />
      <Route
        path="/create-post"
        element={
          <ProtectedRoute>
            <CreatePost />
          </ProtectedRoute>
        }
      />

      {/* 404 */}
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

export default App;
// layouts/MainLayout.tsx
import { Outlet, Link } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';

function MainLayout() {
  const { isAuthenticated, logout } = useAuth();

  return (
    <div className="main-layout">
      <header>
        <nav>
          <Link to="/">Home</Link>
          <Link to="/blog">Blog</Link>
          {isAuthenticated ? (
            <>
              <Link to="/dashboard">Dashboard</Link>
              <Link to="/create-post">Nowy post</Link>
              <button onClick={logout}>Wyloguj</button>
            </>
          ) : (
            <Link to="/login">Zaloguj się</Link>
          )}
        </nav>
      </header>
      <main>
        <Outlet />
      </main>
      <footer>
        <p>© 2024 Blog App</p>
      </footer>
    </div>
  );
}

export default MainLayout;
// pages/Blog.tsx
import { Link, useSearchParams } from 'react-router-dom';
import { useState, useEffect } from 'react';

interface Post {
  id: number;
  title: string;
  excerpt: string;
  category: string;
}

function Blog() {
  const [posts, setPosts] = useState<Post[]>([]);
  const [searchParams, setSearchParams] = useSearchParams();
  
  const category = searchParams.get('category') || 'all';

  useEffect(() => {
    async function fetchPosts() {
      const url = category === 'all' 
        ? '/api/posts' 
        : `/api/posts?category=${category}`;
      const response = await fetch(url);
      const data = await response.json();
      setPosts(data);
    }
    fetchPosts();
  }, [category]);

  return (
    <div className="blog">
      <h1>Blog</h1>
      
      {/* Filtry */}
      <div className="filters">
        <button onClick={() => setSearchParams({ category: 'all' })}>
          Wszystkie
        </button>
        <button onClick={() => setSearchParams({ category: 'tech' })}>
          Technologia
        </button>
        <button onClick={() => setSearchParams({ category: 'lifestyle' })}>
          Lifestyle
        </button>
      </div>

      {/* Lista postów */}
      <div className="posts">
        {posts.map(post => (
          <article key={post.id}>
            <h2>
              <Link to={`/blog/${post.id}`}>{post.title}</Link>
            </h2>
            <p>{post.excerpt}</p>
            <span className="category">{post.category}</span>
          </article>
        ))}
      </div>
    </div>
  );
}

export default Blog;
// pages/PostDetails.tsx
import { useParams, useNavigate } from 'react-router-dom';
import { useState, useEffect } from 'react';

interface Post {
  id: number;
  title: string;
  content: string;
  author: string;
  date: string;
}

function PostDetails() {
  const { postId } = useParams<{ postId: string }>();
  const navigate = useNavigate();
  const [post, setPost] = useState<Post | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchPost() {
      try {
        const response = await fetch(`/api/posts/${postId}`);
        if (!response.ok) {
          navigate('/blog'); // Post nie istnieje, wróć do listy
          return;
        }
        const data = await response.json();
        setPost(data);
      } catch (error) {
        console.error('Error:', error);
        navigate('/blog');
      } finally {
        setLoading(false);
      }
    }
    fetchPost();
  }, [postId, navigate]);

  if (loading) return <div>Ładowanie...</div>;
  if (!post) return null;

  return (
    <article className="post-details">
      <button onClick={() => navigate(-1)}>← Wróć</button>
      <h1>{post.title}</h1>
      <div className="meta">
        <span>Autor: {post.author}</span>
        <span>Data: {post.date}</span>
      </div>
      <div className="content">
        {post.content}
      </div>
    </article>
  );
}

export default PostDetails;
Best Practices

1. Organizuj trasy w oddzielnym pliku

// routes.tsx
import { RouteObject } from 'react-router-dom';
import Home from './pages/Home';
import Blog from './pages/Blog';
// ...

export const routes: RouteObject[] = [
  {
    path: '/',
    element: <MainLayout />,
    children: [
      { index: true, element: <Home /> },
      { path: 'blog', element: <Blog /> },
      { path: 'blog/:postId', element: <PostDetails /> }
    ]
  },
  // ...
];

// App.tsx
import { useRoutes } from 'react-router-dom';
import { routes } from './routes';

function App() {
  return useRoutes(routes);
}

2. Typuj params i search params

// TypeScript dla parametrów
const { userId } = useParams<{ userId: string }>();

// Lub stwórz type
type UserParams = {
  userId: string;
};

const params = useParams<UserParams>();

3. Centralizuj ścieżki

// routes/paths.ts
export const PATHS = {
  HOME: '/',
  BLOG: '/blog',
  BLOG_POST: (id: number) => `/blog/${id}`,
  DASHBOARD: '/dashboard',
  LOGIN: '/login'
} as const;

// Użycie
<Link to={PATHS.BLOG_POST(123)}>Post 123</Link>
navigate(PATHS.DASHBOARD);

4. Lazy loading dla routes

import { lazy, Suspense } from 'react';

const Blog = lazy(() => import('./pages/Blog'));
const Dashboard = lazy(() => import('./pages/Dashboard'));

function App() {
  return (
    <Routes>
      <Route
        path="/blog"
        element={
          <Suspense fallback={<div>Ładowanie...</div>}>
            <Blog />
          </Suspense>
        }
      />
      <Route
        path="/dashboard"
        element={
          <Suspense fallback={<div>Ładowanie...</div>}>
            <Dashboard />
          </Suspense>
        }
      />
    </Routes>
  );
}

Lazy loading ładuje komponenty dopiero gdy są potrzebne – lepsza wydajność!

5. Scroll to top po nawigacji

// components/ScrollToTop.tsx
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function ScrollToTop() {
  const { pathname } = useLocation();

  useEffect(() => {
    window.scrollTo(0, 0);
  }, [pathname]);

  return null;
}

export default ScrollToTop;

// App.tsx
<BrowserRouter>
  <ScrollToTop />
  <App />
</BrowserRouter>
Podsumowanie

To był obszerny, ale bardzo praktyczny wpis! Nauczyliśmy się:

  • React Router – instalacja i konfiguracja
  • Podstawowe trasyRoutes, Route, path, element
  • NawigacjaLink, NavLink, useNavigate
  • Parametry URLuseParams, dynamic segments
  • Query paramsuseSearchParams
  • Zagnieżdżone trasyOutlet, nested routing
  • Protected Routes – autoryzacja, przekierowania
  • Layout Routes – wspólny layout dla wielu tras
  • Kompletny przykład – Blog App z routingiem
  • Best Practices – organizacja, typowanie, lazy loading

React Router to fundament większości aplikacji React. Teraz masz wszystkie narzędzia do budowania wielostronicowych SPA z pełną nawigacją!

W kolejnym wpisie poznamy różne sposoby stylowania w React – CSS Modules, Styled Components, Tailwind CSS. Nauczymy się jak pisać responsywne, skalowalne style!

Zadanie dla Ciebie

Rozbuduj swoją Todo App:

  1. Dodaj routing: / (lista), /completed (ukończone), /active (aktywne)
  2. Dodaj trasę /todo/:id z szczegółami todo
  3. Dodaj protected route /admin dostępną tylko dla admina
  4. (Bonus) Dodaj lazy loading dla tras