Вкладені маршрути та Outlet: архітектура складних макетів
Вкладені маршрути та Outlet: архітектура складних макетів
🔗 Офіційний референс: React Router v7 - Routing (Nested Routes)
🎯 Мета лекції
- Опанувати концепцію вкладених маршрутів для створення ієрархічної структури сторінок.
- Навчитися використовувати
<Outlet>для рендерингу дочірніх компонентів. - Застосувати Layout Routes для створення спільних обгорток без впливу на URL.
- Зрозуміти роль Index Routes як сторінок за замовчуванням.
🔑 Ключові терміни
- Nested Routes (Вкладені маршрути): дочірні маршрути всередині батьківського маршруту, що успадковують його URL-шлях.
- Outlet: компонент-заповнювач, через який рендериться активний дочірній маршрут.
- Layout Route: маршрут без
path, що створює обгортку для дочірніх компонентів без зміни URL. - Index Route: маршрут за замовчуванням, що рендериться у
<Outlet>батьківського маршруту за його базового URL.
Проблема плоскої структури маршрутів
У попередній лекції ми створили просту навігацію:
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/contact" element={<ContactPage />} />
</Routes>
Це працює для невеликих застосунків, але має обмеження:
Проблема 1: Дублювання коду спільних елементів
Якщо кожна сторінка має спільну навігацію, футер або сайдбар, доводиться дублювати цей код у кожному компоненті:
function AboutPage() {
return (
<>
<Navigation /> {/* Дублювання */}
<div>
<h1>Про нас</h1>
<p>Контент...</p>
</div>
<Footer /> {/* Дублювання */}
</>
);
}
Проблема 2: Відсутність ієрархії
Реальні застосунки мають ієрархічну структуру:
/dashboard
/dashboard/profile
/dashboard/settings
/dashboard/analytics
Плоска структура не відображає цю ієрархію та не дозволяє створити спільний Layout для всіх сторінок Dashboard.
Базовий приклад вкладених маршрутів
Створимо Dashboard із трьома підсторінками:
import { Routes, Route, Link, Outlet } from "react-router";
// Layout компонент для Dashboard
function DashboardLayout() {
return (
<div style={{ display: "flex" }}>
<aside style={{ width: "200px", borderRight: "1px solid #ccc", padding: "1rem" }}>
<h2>Dashboard</h2>
<nav>
<ul>
<li><Link to="/dashboard">Огляд</Link></li>
<li><Link to="/dashboard/profile">Профіль</Link></li>
<li><Link to="/dashboard/settings">Налаштування</Link></li>
</ul>
</nav>
</aside>
<main style={{ flex: 1, padding: "1rem" }}>
{/* Тут рендериться активний дочірній маршрут */}
<Outlet />
</main>
</div>
);
}
function DashboardOverview() {
return (
<div>
<h1>Огляд Dashboard</h1>
<p>Статистика та загальна інформація.</p>
</div>
);
}
function ProfilePage() {
return (
<div>
<h1>Профіль користувача</h1>
<p>Редагування особистих даних.</p>
</div>
);
}
function SettingsPage() {
return (
<div>
<h1>Налаштування</h1>
<p>Конфігурація застосунку.</p>
</div>
);
}
function App() {
return (
<Routes>
<Route path="/" element={<HomePage />} />
{/* Батьківський маршрут із Layout */}
<Route path="/dashboard" element={<DashboardLayout />}>
{/* Дочірні маршрути */}
<Route index element={<DashboardOverview />} />
<Route path="profile" element={<ProfilePage />} />
<Route path="settings" element={<SettingsPage />} />
</Route>
</Routes>
);
}
- Батьківський маршрут
path="/dashboard"рендерить<DashboardLayout>. - Дочірні маршрути не мають префікса
/dashboardу своємуpath— це автоматично додається батьківським маршрутом. <Outlet />уDashboardLayout— це місце, де React Router рендерить активний дочірній маршрут.indexмаршрут — спеціальний маршрут безpath, що рендериться за URL/dashboard(базовий URL батька).
- URL
/dashboard→ рендериться<DashboardLayout>+<DashboardOverview>у<Outlet>. - URL
/dashboard/profile→ рендериться<DashboardLayout>+<ProfilePage>у<Outlet>. - URL
/dashboard/settings→ рендериться<DashboardLayout>+<SettingsPage>у<Outlet>.
Index Routes: сторінка за замовчуванням
index — це спеціальний атрибут маршруту, що означає "рендерити цей компонент за базового URL батька".
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardOverview />} />
<Route path="profile" element={<ProfilePage />} />
</Route>
Без index маршруту URL /dashboard рендерував би <DashboardLayout> з порожнім <Outlet> — користувач бачив би тільки сайдбар без контенту. index вирішує цю проблему, надаючи контент за замовчуванням.
index.html:Так само, як веб-сервер показує index.html за URL /folder/, React Router рендерить index маршрут за URL батьківського шляху.Layout Routes: обгортки без впливу на URL
Іноді потрібна спільна обгортка для групи маршрутів без додавання сегмента до URL. Це досягається через Layout Route — маршрут без path:
<Routes>
{/* Layout для публічних сторінок (без auth) */}
<Route element={<PublicLayout />}>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/contact" element={<ContactPage />} />
</Route>
{/* Layout для приватних сторінок (з auth) */}
<Route element={<PrivateLayout />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
</Route>
</Routes>
PublicLayout та PrivateLayout рендеруються для відповідних груп маршрутів, але не додають жодного сегмента до URL.
function PublicLayout() {
return (
<>
<header>
<nav>{/* Публічна навігація */}</nav>
</header>
<Outlet /> {/* Рендериться HomePage, AboutPage або ContactPage */}
<footer>{/* Футер */}</footer>
</>
);
}
function PrivateLayout() {
return (
<>
<header>
<nav>{/* Навігація для авторизованих користувачів */}</nav>
</header>
<Outlet /> {/* Рендериться Dashboard, Profile або Settings */}
<footer>{/* Футер */}</footer>
</>
);
}
- Різних макетів для публічних та приватних сторінок.
- Обгортки авторизації (перевірка токену, редирект на логін).
- Різних тем або мов для груп сторінок.
Глибока вкладеність: маршрути у три рівні
Вкладені маршрути можуть йти на будь-яку глибину:
<Routes>
<Route path="/projects" element={<ProjectsLayout />}>
<Route index element={<ProjectsList />} />
<Route path=":projectId" element={<ProjectLayout />}>
<Route index element={<ProjectOverview />} />
<Route path="tasks" element={<TasksList />} />
<Route path="team" element={<TeamMembers />} />
<Route path="settings" element={<ProjectSettings />} />
</Route>
</Route>
</Routes>
Структура URL:
/projects→<ProjectsList>/projects/abc-123→<ProjectOverview>(огляд проєкту ABC-123)/projects/abc-123/tasks→<TasksList>(список задач проєкту)/projects/abc-123/team→<TeamMembers>(команда проєкту)
Ієрархія компонентів:
ProjectsLayout
└─ Outlet → ProjectLayout
└─ Outlet → ProjectOverview / TasksList / TeamMembers / ProjectSettings
function ProjectsLayout() {
return (
<div>
<h1>Проєкти</h1>
<nav>{/* Навігація проєктів */}</nav>
<Outlet />
</div>
);
}
function ProjectLayout() {
return (
<div style={{ display: "flex" }}>
<aside>
{/* Сайдбар конкретного проєкту */}
<nav>
<Link to="">Огляд</Link>
<Link to="tasks">Задачі</Link>
<Link to="team">Команда</Link>
<Link to="settings">Налаштування</Link>
</nav>
</aside>
<main>
<Outlet />
</main>
</div>
);
}
ProjectLayout посилання <Link to="tasks"> автоматично розуміється як відносне до поточного URL. Якщо ви на /projects/abc-123, клік на "Задачі" веде до /projects/abc-123/tasks.Це дозволяє створювати переважні компоненти, які не знають свого абсолютного URL — вони працюють на основі відносних шляхів.Префікси маршрутів без елемента
Іноді потрібно додати загальний префікс для групи маршрутів без створення Layout-компонента. Це можна зробити через <Route path> без element:
<Routes>
<Route path="/admin">
<Route index element={<AdminDashboard />} />
<Route path="users" element={<UsersList />} />
<Route path="posts" element={<PostsList />} />
<Route path="settings" element={<AdminSettings />} />
</Route>
</Routes>
Це створює маршрути:
/admin→<AdminDashboard>/admin/users→<UsersList>/admin/posts→<PostsList>/admin/settings→<AdminSettings>
Без спільного Layout-компонента — кожен маршрут рендериться безпосередньо.
element:<Route path="/admin" element={<AdminLayout />}>
<Route index element={<AdminDashboard />} />
<Route path="users" element={<UsersList />} />
</Route>
element корисний лише для логічної групування маршрутів без візуальної обгортки.Практичне завдання
Створіть структуру маршрутів для блогу:
- Кореневий рівень:
/— Головна сторінка/blog— Список статей блогу
- Вкладений рівень (Blog Layout):
/blog— Список усіх статей (index)/blog/:postId— Конкретна стаття
- Глибша вкладеність (Post Layout):
/blog/:postId— Текст статті (index)/blog/:postId/comments— Коментарі до статті/blog/:postId/related— Схожі статті
Вимоги:
BlogLayoutмає містити сайдбар із категоріями.PostLayoutмає містити навігацію між вкладками (Стаття / Коментарі / Схожі).- Використайте
<Outlet>у кожному Layout.
import { Routes, Route, Link, Outlet, useParams } from "react-router";
// Головна сторінка
function HomePage() {
return (
<div>
<h1>Ласкаво просимо</h1>
<Link to="/blog">Перейти до блогу</Link>
</div>
);
}
// Layout для всього блогу
function BlogLayout() {
return (
<div style={{ display: "flex" }}>
<aside style={{ width: "200px", padding: "1rem", borderRight: "1px solid #ccc" }}>
<h2>Блог</h2>
<nav>
<ul>
<li><Link to="/blog">Усі статті</Link></li>
<li><Link to="/blog">Технології</Link></li>
<li><Link to="/blog">Дизайн</Link></li>
</ul>
</nav>
</aside>
<main style={{ flex: 1, padding: "1rem" }}>
<Outlet />
</main>
</div>
);
}
// Список статей
function PostsList() {
return (
<div>
<h1>Усі статті</h1>
<ul>
<li><Link to="/blog/react-hooks">Як працюють React Hooks</Link></li>
<li><Link to="/blog/typescript-tips">TypeScript поради</Link></li>
<li><Link to="/blog/css-grid">Розуміння CSS Grid</Link></li>
</ul>
</div>
);
}
// Layout для конкретної статті
function PostLayout() {
const { postId } = useParams();
return (
<div>
<h1>Стаття: {postId}</h1>
<nav style={{ borderBottom: "1px solid #ccc", marginBottom: "1rem" }}>
<Link to={`/blog/${postId}`} style={{ marginRight: "1rem" }}>
Стаття
</Link>
<Link to={`/blog/${postId}/comments`} style={{ marginRight: "1rem" }}>
Коментарі
</Link>
<Link to={`/blog/${postId}/related`}>
Схожі статті
</Link>
</nav>
<Outlet />
</div>
);
}
// Текст статті
function PostContent() {
const { postId } = useParams();
return (
<div>
<p>Текст статті {postId}...</p>
</div>
);
}
// Коментарі
function PostComments() {
return (
<div>
<h2>Коментарі</h2>
<p>Тут будуть коментарі...</p>
</div>
);
}
// Схожі статті
function RelatedPosts() {
return (
<div>
<h2>Схожі статті</h2>
<ul>
<li>Стаття А</li>
<li>Стаття Б</li>
</ul>
</div>
);
}
function App() {
return (
<Routes>
<Route path="/" element={<HomePage />} />
{/* Вкладені маршрути блогу */}
<Route path="/blog" element={<BlogLayout />}>
<Route index element={<PostsList />} />
{/* Глибша вкладеність для конкретної статті */}
<Route path=":postId" element={<PostLayout />}>
<Route index element={<PostContent />} />
<Route path="comments" element={<PostComments />} />
<Route path="related" element={<RelatedPosts />} />
</Route>
</Route>
</Routes>
);
}
export default App;
Пояснення:
- Перший рівень вкладеності:
/blogрендерить<BlogLayout>із сайдбаром категорій та<Outlet>для дочірніх маршрутів. - Другий рівень:
/blog/:postIdрендерить<PostLayout>із навігацією між вкладками та власним<Outlet>. - Третій рівень: Вкладки (Стаття / Коментарі / Схожі) рендеряться у
<Outlet>компонентаPostLayout.
URL-шляхи:
/blog→ BlogLayout + PostsList/blog/react-hooks→ BlogLayout + PostLayout + PostContent/blog/react-hooks/comments→ BlogLayout + PostLayout + PostComments/blog/react-hooks/related→ BlogLayout + PostLayout + RelatedPosts
Підсумок
✅ Що ми опанували
- Організацію вкладених маршрутів через ієрархічну структуру
<Route>. - Використання
<Outlet>для рендерингу активного дочірнього компонента. - Index Routes як сторінки за замовчуванням для базового URL батька.
- Layout Routes (без
path) для створення обгорток без впливу на URL. - Багаторівневу вкладеність для складних застосунків.
🎯 Ключові переваги
- Усунення дублювання коду: спільні Layout-компоненти визначаються один раз.
- Чітка ієрархія: структура маршрутів відображає структуру UI.
- Відносні посилання: дочірні компоненти не знають свого абсолютного URL.
- Масштабованість: легко додавати нові рівні вкладеності без рефакторингу існуючого коду.
Встановлення та базова конфігурація React Router v7
Встановлення React Router v7 як бібліотеки для SPA-маршрутизації, налаштування BrowserRouter, перші маршрути через Routes та Route, розуміння декларативного підходу до навігації.
Динамічні маршрути та параметри URL
Робота з динамічними сегментами URL через Route Params, витягування значень через useParams, необов'язкові сегменти, wildcard-маршрути (splat), типізація параметрів у TypeScript.