React Router - základní routing 🛣️
React Router je standardní knihovna pro routing v React aplikacích. Umožňuje nám vytvářet Single Page Applications (SPA) s navigací mezi různými stránkami (komponentami, které nazveme stránkami).
Single Page Application je aplikace, která se skládá z jedné stránky. Načte se jednou a poté podle adresy a parametrů se načítají různé stránky nebo komponenty (viděli jsme na cvičení použití
query parametru).Router toho ale umí ještě trošku víc.
🎯 Co je React Router?
React Router poskytuje deklarativní routing (vykreslení konkrétní komponenty pro danou adresu) v React aplikaci.
Umožňuje nám definovat, které komponenty se zobrazí na základě URL adresy.
Výhody React Routeru:
✅ SPA navigace - rychlé přepínání mezi stránkami
✅ URL synchronizace - URL odpovídá aktuální stránce
✅ Browser history - pracuje s historií v prohlížeči - praktický dopad je, že tlačítka zpět/dopředu fungují i bez reloadu stránky
✅ Nested routes - vnořené cesty - můžeme vytvářet vnořené cesty a skupiny tzv. rout
✅ Route guards - můžeme přidat nějaké podmíny - např. sekci pouze pro přihlášené uživatele
🚀 Instalace a setup
Instalace:
npm install react-router-dom
Základní setup:
// App.jsx
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
import EmployeeList from './pages/EmployeeList';
import EmployeeDetail from './pages/EmployeeDetail';
import AddEmployee from './pages/AddEmployee';
import Dashboard from './pages/Dashboard';
const App = () => {
return (
<Router>
<div className="min-h-screen bg-gray-50">
<Navigation />
<main className="max-w-6xl mx-auto px-4 py-8">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/employees" element={<EmployeeList />} />
<Route path="/employees/:id" element={<EmployeeDetail />} />
<Route path="/employees/add" element={<AddEmployee />} />
<Route path="*" element={<NotFound />} />
</Routes>
</main>
</div>
</Router>
);
};
const Navigation = () => {
return (
<nav className="bg-white shadow-sm border-b border-gray-200">
<div className="max-w-6xl mx-auto px-4 py-4">
<div className="flex items-center justify-between">
<Link to="/" className="text-xl font-bold text-gray-900">
Employee Manager
</Link>
<div className="flex space-x-4">
<Link
to="/"
className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
>
Dashboard
</Link>
<Link
to="/employees"
className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
>
Zaměstnanci
</Link>
<Link
to="/employees/add"
className="bg-blue-500 hover:bg-blue-600 text-white px-3 py-2 rounded-md text-sm font-medium"
>
Přidat zaměstnance
</Link>
</div>
</div>
</div>
</nav>
);
};
const NotFound = () => {
return (
<div className="text-center py-12">
<h1 className="text-4xl font-bold text-gray-900 mb-4">404</h1>
<p className="text-gray-600 mb-8">Stránka nebyla nalezena</p>
<Link
to="/"
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md"
>
Zpět na hlavní stránku
</Link>
</div>
);
};
export default App;
🎨 Základní komponenty
1. EmployeeList stránka:
// pages/EmployeeList.jsx
import React, { useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { fetchEmployees, setFilter } from '../store/slices/employeeSlice';
import EmployeeCard from '../components/EmployeeCard';
import FilterBar from '../components/FilterBar';
const EmployeeList = () => {
const dispatch = useDispatch();
const { employees, filter, loading, error } = useSelector(state => state.employees);
useEffect(() => {
dispatch(fetchEmployees());
}, [dispatch]);
const filteredEmployees = filter === 'all'
? employees
: employees.filter(emp => emp.department === filter);
if (loading) {
return (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500 mx-auto"></div>
</div>
);
}
if (error) {
return (
<div className="text-center py-12">
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<strong className="font-bold">Chyba!</strong>
<span className="block sm:inline"> {error}</span>
</div>
</div>
);
}
return (
<div className="w-full">
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold text-gray-900">Seznam zaměstnanců</h1>
<Link
to="/employees/add"
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md font-medium"
>
Přidat zaměstnance
</Link>
</div>
<FilterBar
currentFilter={filter}
onFilterChange={(newFilter) => dispatch(setFilter(newFilter))}
/>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{filteredEmployees.map(employee => (
<EmployeeCard
key={employee.id}
employee={employee}
/>
))}
</div>
</div>
);
};
export default EmployeeList;
2. EmployeeDetail stránka:
// pages/EmployeeDetail.jsx
import React, { useEffect } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { fetchEmployees, updateEmployee, deleteEmployee } from '../store/slices/employeeSlice';
const EmployeeDetail = () => {
const { id } = useParams();
const navigate = useNavigate();
const dispatch = useDispatch();
const { employees, loading, error } = useSelector(state => state.employees);
const employee = employees.find(emp => emp.id === parseInt(id));
useEffect(() => {
if (employees.length === 0) {
dispatch(fetchEmployees());
}
}, [dispatch, employees.length]);
const handleDelete = async () => {
if (window.confirm('Opravdu chcete smazat tohoto zaměstnance?')) {
await dispatch(deleteEmployee(parseInt(id)));
navigate('/employees');
}
};
const handleEdit = () => {
navigate(`/employees/${id}/edit`);
};
if (loading) {
return (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500 mx-auto"></div>
</div>
);
}
if (error) {
return (
<div className="text-center py-12">
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<strong className="font-bold">Chyba!</strong>
<span className="block sm:inline"> {error}</span>
</div>
<Link
to="/employees"
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md"
>
Zpět na seznam
</Link>
</div>
);
}
if (!employee) {
return (
<div className="text-center py-12">
<h1 className="text-2xl font-bold text-gray-900 mb-4">Zaměstnanec nebyl nalezen</h1>
<Link
to="/employees"
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md"
>
Zpět na seznam
</Link>
</div>
);
}
return (
<div className="max-w-4xl mx-auto">
<div className="mb-6">
<Link
to="/employees"
className="text-blue-600 hover:text-blue-800 font-medium"
>
← Zpět na seznam
</Link>
</div>
<div className="bg-white rounded-lg shadow-md p-6">
<div className="flex items-start justify-between mb-6">
<div className="flex items-center">
<div className="w-16 h-16 bg-blue-500 rounded-full flex items-center justify-center text-white font-bold text-2xl mr-4">
{employee.name.charAt(0)}
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900">{employee.name}</h1>
<p className="text-xl text-gray-600">{employee.position}</p>
<span className="inline-block bg-blue-100 text-blue-800 text-sm font-medium px-2.5 py-0.5 rounded">
{employee.department}
</span>
</div>
</div>
<div className="flex space-x-2">
<button
onClick={handleEdit}
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md font-medium"
>
Upravit
</button>
<button
onClick={handleDelete}
className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md font-medium"
>
Smazat
</button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">Kontaktní informace</h3>
<div className="space-y-3">
<div>
<label className="text-sm font-medium text-gray-500">Email</label>
<p className="text-gray-900">{employee.email}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-500">Telefon</label>
<p className="text-gray-900">{employee.phone}</p>
</div>
</div>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">Další informace</h3>
<div className="space-y-3">
<div>
<label className="text-sm font-medium text-gray-500">Datum nástupu</label>
<p className="text-gray-900">{employee.hireDate}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-500">Mzda</label>
<p className="text-gray-900">{employee.salary?.toLocaleString()} Kč</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default EmployeeDetail;
🎨 Pokročilé funkce
1. Nested routes:
// App.jsx
<Routes>
<Route path="/employees" element={<EmployeeLayout />}>
<Route index element={<EmployeeList />} />
<Route path="add" element={<AddEmployee />} />
<Route path=":id" element={<EmployeeDetail />} />
<Route path=":id/edit" element={<EditEmployee />} />
</Route>
</Routes>
// components/EmployeeLayout.jsx
import { Outlet } from 'react-router-dom';
const EmployeeLayout = () => {
return (
<div>
<div className="bg-blue-50 p-4 mb-6">
<h2 className="text-xl font-semibold text-blue-900">Správa zaměstnanců</h2>
</div>
<Outlet /> {/* Zde se zobrazí child routes */}
</div>
);
};
2. Route guards:
// components/ProtectedRoute.jsx
import { Navigate } from 'react-router-dom';
const ProtectedRoute = ({ children, isAuthenticated }) => {
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return children;
};
// Použití
<Route
path="/employees"
element={
<ProtectedRoute isAuthenticated={isLoggedIn}>
<EmployeeList />
</ProtectedRoute>
}
/>
3. Programmatic navigation:
// components/EmployeeCard.jsx
import { useNavigate } from 'react-router-dom';
const EmployeeCard = ({ employee }) => {
const navigate = useNavigate();
const handleClick = () => {
navigate(`/employees/${employee.id}`);
};
return (
<div
className="bg-white rounded-lg p-4 shadow-md cursor-pointer hover:shadow-lg transition-shadow"
onClick={handleClick}
>
<h3 className="text-lg font-semibold">{employee.name}</h3>
<p className="text-gray-600">{employee.position}</p>
</div>
);
};
📋 Praktické cvičení
- Nastavte React Router v aplikaci
- Vytvořte základní stránky (Dashboard, EmployeeList, EmployeeDetail)
- Implementujte navigaci mezi stránkami
- Přidejte nested routes pro lepší organizaci
- Implementujte route guards pro ochranu stránek
🚀 Tipy a triky
1. Active link styling:
import { useLocation } from 'react-router-dom';
const Navigation = () => {
const location = useLocation();
return (
<nav>
<Link
to="/employees"
className={`px-3 py-2 rounded-md text-sm font-medium ${
location.pathname === '/employees'
? 'bg-blue-500 text-white'
: 'text-gray-600 hover:text-gray-900'
}`}
>
Zaměstnanci
</Link>
</nav>
);
};
2. Query parameters:
import { useSearchParams } from 'react-router-dom';
const EmployeeList = () => {
const [searchParams, setSearchParams] = useSearchParams();
const filter = searchParams.get('filter') || 'all';
const handleFilterChange = (newFilter) => {
setSearchParams({ filter: newFilter });
};
};
3. Hash routing:
// Místo BrowserRouter použijte HashRouter pro statické hosting
import { HashRouter as Router } from 'react-router-dom';
React Router je nezbytný nástroj pro vytváření SPA aplikací. V příští části se podíváme na routing hooks! 🚀