Тема: Розробка багатосторінкової навігації.
Мета: Навчитися проектувати архітектуру багаторівневої навігації в односторінкових додатках; опанувати механізми декларативної маршрутизації за допомогою бібліотеки react-router-dom; реалізувати систему вкладених макетів (Layouts) та динамічних параметрів; вивчити принципи програмної навігації та захисту доступу до розділів додатку.
Технологічний стек: React, Vite, React Router v6, CSS Modules, Hooks (useParams, useNavigate, useLocation).
react-router-dom у розроблений проект стрічки нових (з Лабораторної роботи №3) та налаштувати базовий провайдер маршрутизації.*.Сучасний фронтенд-інструментарій, такий як Vite, забезпечує високу швидкість розробки завдяки використанню нативних ES-модулів (ESM), що дозволяє серверу розробки стартувати миттєво навіть у великих проектах. На відміну від застарілого Webpack, Vite не збирає весь проект заздалегідь, а віддає файли браузеру лише тоді, коли той їх запитує. У такому середовищі інсталяція react-router-dom є першим критичним кроком для перетворення набору ізольованих компонентів у цілісну систему.
Команда для встановлення:
npm install react-router-dom
Після встановлення необхідно ініціалізувати маршрутизатор у точці входу додатка. Використання BrowserRouter є стандартом для веб-додатків, оскільки він використовує HTML5 History API для синхронізації UI з URL. Це забезпечує чисті URL-адреси (без символу #), які краще індексуються пошуковими системами та зручніші для користувачів.
src/main.jsx:
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>,
);
У React Router v6 компонент Routes прийшов на зміну Switch. Головна архітектурна зміна полягає в алгоритмі ранжування маршрутів. У версії v5 розробник мав ретельно стежити за порядком визначення маршрутів, оскільки система обирала перший збіг, що часто призводило до потреби використовувати проп exact. У v6 впроваджено інтелектуальний алгоритм, який оцінює кожен шлях за його специфічністю, де статичні сегменти мають вищий пріоритет над динамічними параметрами.
src/App.jsx:
import { Routes, Route } from "react-router-dom";
import MainLayout from "./components/templates/MainLayout/MainLayout";
import Home from "./pages/Home/Home";
import Feed from "./pages/Feed/Feed";
import PostPage from "./pages/PostPage/PostPage";
import Profile from "./pages/Profile/Profile";
import NotFound from "./pages/NotFound/NotFound";
function App() {
return (
<Routes>
<Route path="/" element={<MainLayout />}>
<Route index element={<Home />} />
<Route path="feed" element={<Feed />} />
<Route path="feed/:postId" element={<PostPage />} />
<Route path="profile/*" element={<Profile />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
);
}
export default App;
Важливою частиною цього кроку є розуміння патерна “Layout Route”. Коли Route не має властивості path, він стає макетом, який огортає всі вкладені маршрути, надаючи їм спільну структуру (наприклад, Header та Footer) без додавання нових сегментів до URL.
Компонент Outlet є фундаментальною інновацією v6. Він діє як динамічний заповнювач, у який рендеряться дочірні елементи поточного маршруту. Це дозволяє уникнути повторного рендерингу незмінних частин інтерфейсу, таких як навігаційна панель, що позитивно впливає на продуктивність за рахунок мінімізації операцій у реальному DOM.
src/components/templates/MainLayout/MainLayout.jsx:
import { NavLink, Outlet } from "react-router-dom";
import styles from "./MainLayout.module.css";
const MainLayout = () => {
const getActiveClass = ({ isActive }) =>
isActive ? `${styles.link} ${styles.active}` : styles.link;
return (
<div className={styles.wrapper}>
<nav className={styles.navbar}>
<NavLink to="/" className={getActiveClass} end>
Головна
</NavLink>
<NavLink to="/feed" className={getActiveClass}>
Стрічка
</NavLink>
<NavLink to="/profile" className={getActiveClass}>
Профіль
</NavLink>
</nav>
<main className={styles.mainContent}>
<Outlet />
</main>
<footer className={styles.footer}>
Розроблено в рамках лабораторної роботи №4
</footer>
</div>
);
};
export default MainLayout;
Використання NavLink замість звичайного Link дозволяє декларативно керувати станом активності посилань. Проп end гарантує, що посилання на кореневий шлях / не буде позначене як активне при переході на /feed, оскільки за замовчуванням усі шляхи в React Router є частковими збігами.
Вкладені маршрути дозволяють відображати частину інтерфейсу всередині іншої частини, що ідеально підходить для кабінетів користувача з багатьма вкладками. У v6 можна використовувати відносні шляхи: посилання на settings усередині компонента, що рендериться за адресою /profile, автоматично вестиме на /profile/settings.
src/pages/Profile/Profile.jsx:
import { Routes, Route, Link, Outlet } from "react-router-dom";
import ProfileOverview from "./ProfileOverview";
import ProfileSettings from "./ProfileSettings";
import styles from "./Profile.module.css";
const Profile = () => {
return (
<div className={styles.profileLayout}>
<aside className={styles.sidebar}>
<h3>Мій акаунт</h3>
<Link to="">Інформація</Link>
<Link to="settings">Налаштування</Link>
</aside>
<div className={styles.content}>
<Routes>
<Route index element={<ProfileOverview />} />
<Route path="settings" element={<ProfileSettings />} />
</Routes>
</div>
</div>
);
};
export default Profile;
Така декомпозиція маршрутів робить код більш стійким до змін (DRY - Don’t Repeat Yourself) та полегшує тестування окремих частин додатку.
Для створення сторінок, контент яких залежить від конкретного об’єкта, використовуються динамічні сегменти, що позначаються двокрапкою в шляху (наприклад, :postId). Хук useParams повертає об’єкт, ключами якого є назви параметрів, вказані при визначенні маршруту.
src/pages/PostPage/PostPage.jsx:
import { useParams, useNavigate } from "react-router-dom";
import { postsData } from "../../data";
import styles from "./PostPage.module.css";
const PostPage = () => {
const { postId } = useParams();
const navigate = useNavigate();
const post = postsData.find((p) => p.id === Number(postId));
if (!post) {
return <div className={styles.error}>Пост із ID {postId} не знайдено.</div>;
}
return (
<article className={styles.article}>
<button onClick={() => navigate(-1)} className={styles.backButton}>
← Повернутися
</button>
<header>
<h1>{post.title}</h1>
<p>
Автор: <strong>{post.author}</strong>
</p>
</header>
<div className={styles.body}>{post.content}</div>
</article>
);
};
export default PostPage;
Хук useNavigate дозволяє реалізувати імперативну логіку переходів, що є критично важливим після виконання асинхронних дій або при реалізації кнопок повернення в історії браузера.
При проектуванні систем навігації в React важливо розуміти вплив вибору типу маршрутизатора на підсумкову продуктивність та сумісність.
| Тип маршрутизатора | Механізм роботи | Сценарій використання | Переваги |
|---|---|---|---|
| BrowserRouter | HTML5 History API | Сучасні веб-додатки з підтримкою сервера | Чисті URL, підтримка SEO, стандартна поведінка браузера. |
| HashRouter | URL fragments (#) |
Статичні сервери, GitHub Pages без налаштування | Не потребує серверних налаштувань для обробки 404 помилок. |
| MemoryRouter | In-memory history | Тестування компонентів, Storybook, Mobile | Повністю ізольований від браузерного середовища. |
| StaticRouter | Об’єктний опис локації | Server-Side Rendering (SSR) | Швидка перша отрисовка на сервері. |
Варто також проаналізувати ключові відмінності між мажорними версіями React Router, що визначають сучасні стандарти розробки.
| Функціонал | React Router v5 | React Router v6 | Обґрунтування зміни |
|---|---|---|---|
| Компонент-контейнер | Switch |
Routes |
Routes забезпечує розумніше ранжування та відносну вкладеність. |
| Визначення компонента | component={Page} |
element={<Page />} |
Використання елемента дозволяє передавати пропси напряму та зберігати життєвий цикл компонента. |
| Програмна навігація | useHistory |
useNavigate |
Спрощений API, що об’єднує методи push, replace та go в одну функцію. |
| Збірка та розмір | 100% | ~40% | Повна переробка ядра для кращого tree-shaking та сумісності з сучасними бандлерами. |
Кожна зміна маршруту ініціює цикл узгодження (Reconciliation) у React. Оскільки DOM-операції є ресурсомісткими ($O(n^3)$ складність алгоритму дифінгу в гіршому випадку), React використовує Virtual DOM для мінімізації навантаження на головний потік браузера. При переході між сторінками механізм diffing порівнює дерева віртуальних елементів:
Outlet, що дозволяє досягти плавності інтерфейсу, порівнянної з нативними додатками.Для великих масивів даних, таких як списки новин, критично важливо використовувати стабільні ключі (key). Використання індексів масиву під час навігації по фільтрованим даним може призвести до “Layout Thrashing” — багаторазового перерахунку макету, що спричиняє візуальні затримки (jank).
В архітектурі SPA безпека реалізується через патерн “HOC” (Higher-Order Component) або обгортку для Route. Це дозволяє декларативно перевіряти стан авторизації перед відображенням контенту.
const ProtectedRoute = ({ isAllowed, redirectPath = "/login", children }) => {
if (!isAllowed) {
return <Navigate to={redirectPath} replace />;
}
return children ? children : <Outlet />;
};
Такий підхід дозволяє централізовано керувати правами доступу. Використання властивості replace: true у хуку useNavigate або компоненті Navigate є критично важливим для збереження чистоти історії браузера: воно замінює поточний запис у стеку замість додавання нового, що запобігає нескінченним циклам при натисканні користувачем кнопки “Назад”.
Аналіз сучасних трендів (станом на 2024–2025 роки) вказує на зміщення акценту з чисто клієнтського рендерингу (CSR) у бік гібридних моделей. React Router v7 та фреймворки типу Next.js впроваджують концепцію React Server Components (RSC), де частина логіки маршрутизації знову повертається на сервер для покращення SEO та зменшення обсягу JavaScript, який завантажується клієнтом.
Проте для більшості інтерактивних додатків клієнтська маршрутизація залишається фундаментальною навичкою. Розуміння механізмів useLocation та useSearchParams дозволяє розробникам будувати додатки, де URL виступає як “Single Source of Truth” (Єдине джерело істини) для всього стану інтерфейсу, включаючи фільтри, пагінацію та відкриті модальні вікна.
index у компоненті Route?
Атрибут index вказує, що даний маршрут є типовим (default) для батьківського маршруту. Він відображається у батьківському Outlet, коли URL збігається точно зі шляхом батька.useNavigate, а не Link?
Link є декларативним компонентом для створення клікабельних елементів у JSX. useNavigate повертає функцію, яку можна викликати всередині будь-якої логіки (наприклад, після завершення fetch-запиту або валідації форми), що забезпечує гнучкість управління потоком навігації.NavLink. Його властивості className та style можуть приймати функцію, яка отримує аргумент зі станом isActive, що дозволяє застосовувати специфічні CSS-класи автоматично.*, який збігається з будь-яким URL. Його необхідно розміщувати останнім у списку Routes, щоб він спрацював лише тоді, коли жоден інший визначений шлях не підійшов.Реалізація багатосторінкової навігації на базі React Router v6 є не просто технічним завданням, а архітектурним проектуванням досвіду користувача. Використання вкладених маршрутів дозволяє будувати масштабовані системи з чітким розподілом обов’язків між компонентами. При розробці професійних систем варто дотримуватися наступних правил:
React.lazy для сторінок, які рідко відвідуються, щоб зменшити початковий розмір додатку.Link під капотом рендерять теги <a>, що є критичним для скрінрідерів та SEO.Звіт повинен бути оформлений у форматі Markdown (lab_04.md) та містити:
Routes та BrowserRouter.Layout із компонентом Outlet.useParams.useNavigate.