React Router
React Router — дорожнє перехрестя з вказівниками напрямків до різних пунктів призначення
У попередніх уроках ми будували компоненти, працювали зі state та context. Але наш додаток -- це одна сторінка. Що, якщо потрібна навігація: головна, профіль, список товарів, сторінка товару? Для цього існує React Router -- бібліотека для маршрутизації в React-додатках.
Встановлення
npm install react-router-dom
React Router працює тільки на клієнті (SPA -- Single Page Application). Сервер завжди віддає один index.html, а React Router перемикає компоненти залежно від URL.
Базова структура
// src/App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import Home from './pages/Home'
import About from './pages/About'
import Contact from './pages/Contact'
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</BrowserRouter>
)
}
export default App
BrowserRouter-- обгортка, що стежить за URLRoutes-- контейнер для маршрутівRoute-- один маршрут:path-- URL,element-- що рендерити
Сторінки -- звичайні компоненти
// src/pages/Home.tsx
export default function Home() {
return (
<div>
<h1>Головна сторінка</h1>
<p>Ласкаво просимо!</p>
</div>
)
}
// src/pages/About.tsx
export default function About() {
return (
<div>
<h1>Про нас</h1>
<p>Ми вчимо React!</p>
</div>
)
}
Link та NavLink -- навігація
Замість HTML-тега <a> використовуй <Link> -- він переходить між сторінками без перезавантаження:
import { Link } from 'react-router-dom'
function Navigation() {
return (
<nav>
<Link to="/">Головна</Link>
<Link to="/about">Про нас</Link>
<Link to="/contact">Контакти</Link>
</nav>
)
}
NavLink -- з активним станом
NavLink автоматично додає клас active для поточного маршруту:
import { NavLink } from 'react-router-dom'
function Navigation() {
return (
<nav>
<NavLink
to="/"
className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}
>
Головна
</NavLink>
<NavLink
to="/about"
className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}
>
Про нас
</NavLink>
</nav>
)
}
Ніколи не використовуй <a href="/about"> для внутрішньої навігації в React-додатку. Тег <a> перезавантажить сторінку та скине весь React-state. Використовуй <Link> або <NavLink>.
Layout з навігацією
Зазвичай навігація та footer однакові на всіх сторінках. Створимо layout:
// src/components/Layout.tsx
import { NavLink, Outlet } from 'react-router-dom'
export default function Layout() {
return (
<div className="app">
<header>
<nav>
<NavLink to="/">Головна</NavLink>
<NavLink to="/about">Про нас</NavLink>
<NavLink to="/products">Товари</NavLink>
</nav>
</header>
<main>
<Outlet /> {/* Тут рендериться поточна сторінка */}
</main>
<footer>
<p>Footer</p>
</footer>
</div>
)
}
// src/App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import Layout from './components/Layout'
import Home from './pages/Home'
import About from './pages/About'
import Products from './pages/Products'
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="products" element={<Products />} />
</Route>
</Routes>
</BrowserRouter>
)
}
<Route path="/" element={<Layout />}>-- Layout обгортає всі вкладені маршрути<Outlet />-- місце, де рендериться вміст вкладеного маршруту<Route index>-- маршрут для/(головна сторінка)
Динамічні маршрути та useParams
Для сторінок на кшталт /products/42 або /users/john:
// src/App.tsx
<Route path="products/:productId" element={<ProductDetail />} />
:productId -- динамічний параметр. Отримуємо його через useParams:
// src/pages/ProductDetail.tsx
import { useParams } from 'react-router-dom'
import { useState, useEffect } from 'react'
interface Product {
id: number
title: string
price: number
description: string
}
export default function ProductDetail() {
const { productId } = useParams<{ productId: string }>()
const [product, setProduct] = useState<Product | null>(null)
useEffect(() => {
async function fetchProduct() {
const res = await fetch(`https://fakestoreapi.com/products/${productId}`)
const data = await res.json()
setProduct(data)
}
fetchProduct()
}, [productId])
if (!product) return <p>Завантаження...</p>
return (
<div>
<h1>{product.title}</h1>
<p>{product.description}</p>
<p className="price">{product.price} $</p>
</div>
)
}
Посилання на динамічну сторінку
import { Link } from 'react-router-dom'
function ProductList({ products }: { products: Product[] }) {
return (
<ul>
{products.map(p => (
<li key={p.id}>
<Link to={`/products/${p.id}`}>{p.title}</Link>
</li>
))}
</ul>
)
}
useNavigate -- програмна навігація
Для переходу після дії (submit форми, клік кнопки):
import { useNavigate } from 'react-router-dom'
function LoginForm() {
const navigate = useNavigate()
function handleSubmit(e: React.FormEvent) {
e.preventDefault()
// ... перевірка логіну ...
// Перехід на головну
navigate('/')
// Або перехід назад
// navigate(-1)
}
return (
<form onSubmit={handleSubmit}>
<input placeholder="Email" />
<input type="password" placeholder="Пароль" />
<button type="submit">Увійти</button>
</form>
)
}
Сторінка 404
// src/pages/NotFound.tsx
import { Link } from 'react-router-dom'
export default function NotFound() {
return (
<div>
<h1>404 -- Сторінку не знайдено</h1>
<p>Такої сторінки не існує.</p>
<Link to="/">На головну</Link>
</div>
)
}
// src/App.tsx
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="products" element={<Products />} />
<Route path="products/:productId" element={<ProductDetail />} />
<Route path="*" element={<NotFound />} /> {/* Будь-який інший URL */}
</Route>
</Routes>
path="*" -- "зловити все" -- спрацьовує, коли жоден інший маршрут не підходить.
Nested routes -- вкладені маршрути
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
{/* Вкладені маршрути для settings */}
<Route path="settings" element={<SettingsLayout />}>
<Route index element={<SettingsGeneral />} />
<Route path="profile" element={<SettingsProfile />} />
<Route path="security" element={<SettingsSecurity />} />
</Route>
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
// src/pages/SettingsLayout.tsx
import { NavLink, Outlet } from 'react-router-dom'
export default function SettingsLayout() {
return (
<div className="settings">
<aside>
<NavLink to="/settings">Загальні</NavLink>
<NavLink to="/settings/profile">Профіль</NavLink>
<NavLink to="/settings/security">Безпека</NavLink>
</aside>
<div className="settings-content">
<Outlet />
</div>
</div>
)
}
Це дає URL: /settings, /settings/profile, /settings/security. Кожен з бічною навігацією.
Структура проекту з routing
src/
├── components/
│ ├── Layout.tsx
│ ├── Navigation.tsx
│ └── ...
├── pages/
│ ├── Home.tsx
│ ├── About.tsx
│ ├── Products.tsx
│ ├── ProductDetail.tsx
│ ├── NotFound.tsx
│ └── settings/
│ ├── SettingsLayout.tsx
│ ├── SettingsGeneral.tsx
│ └── SettingsProfile.tsx
├── contexts/
│ └── ThemeContext.tsx
├── App.tsx
└── main.tsx
Підсумок
- React Router -- бібліотека для client-side routing у SPA
BrowserRouter→Routes→Route-- базова структура- Link/NavLink -- навігація без перезавантаження. NavLink має
isActive - Outlet -- місце рендерингу вкладеного маршруту
- useParams -- отримати параметри з URL (
:productId) - useNavigate -- програмна навігація (після submit, за умовою)
path="*"-- 404 сторінка- Nested routes -- для складних layouts з суб-навігацією
Що далі?
У фінальному уроці блоку ми поєднаємо все вивчене у практичному проекті -- повноцінний React-додаток з API, routing, state management та context.
Корисні посилання:
- React Router -- офіційна документація -- повна документація v7
- React Router: Tutorial -- покроковий туторіал
- React Router: useParams -- навігація та параметри