React Router v7: Маршрутизація в SPA

Вкладені маршрути та Outlet: архітектура складних макетів

Організація вкладеної маршрутизації через Route-дерево, використання Outlet для рендерингу дочірніх компонентів, Layout Routes для спільних обгорток, Index Routes як сторінки за замовчуванням.

Вкладені маршрути та 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.

Вкладені маршрути вирішують обидві проблеми: вони дозволяють визначити спільну обгортку (Layout) один раз, а дочірні маршрути успадковують цю обгортку та автоматично додають свій сегмент до URL батьківського маршруту.

Базовий приклад вкладених маршрутів

Створимо 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>
  );
}
Ключові моменти:
  1. Батьківський маршрут path="/dashboard" рендерить <DashboardLayout>.
  2. Дочірні маршрути не мають префікса /dashboard у своєму path — це автоматично додається батьківським маршрутом.
  3. <Outlet /> у DashboardLayout — це місце, де React Router рендерить активний дочірній маршрут.
  4. 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>
    </>
  );
}
Layout Routes ідеально підходять для:
  • Різних макетів для публічних та приватних сторінок.
  • Обгортки авторизації (перевірка токену, редирект на логін).
  • Різних тем або мов для груп сторінок.

Глибока вкладеність: маршрути у три рівні

Вкладені маршрути можуть йти на будь-яку глибину:

<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-компонента — кожен маршрут рендериться безпосередньо.

Якщо потрібен спільний макет, краще використовувати Layout Route з element:
<Route path="/admin" element={<AdminLayout />}>
  <Route index element={<AdminDashboard />} />
  <Route path="users" element={<UsersList />} />
</Route>
Префікс без element корисний лише для логічної групування маршрутів без візуальної обгортки.

Практичне завдання


Підсумок

✅ Що ми опанували

  • Організацію вкладених маршрутів через ієрархічну структуру <Route>.
  • Використання <Outlet> для рендерингу активного дочірнього компонента.
  • Index Routes як сторінки за замовчуванням для базового URL батька.
  • Layout Routes (без path) для створення обгорток без впливу на URL.
  • Багаторівневу вкладеність для складних застосунків.

🎯 Ключові переваги

  • Усунення дублювання коду: спільні Layout-компоненти визначаються один раз.
  • Чітка ієрархія: структура маршрутів відображає структуру UI.
  • Відносні посилання: дочірні компоненти не знають свого абсолютного URL.
  • Масштабованість: легко додавати нові рівні вкладеності без рефакторингу існуючого коду.
Copyright © 2026