Вложенные маршруты

Haunted — это библиотека для создания реактивных веб-компонентов на базе стандартов Web Components и Hooks, вдохновлённых React. Одной из ключевых возможностей при построении SPA (Single Page Application) является управление маршрутизацией. В сложных приложениях часто возникает необходимость вложенных маршрутов, когда один компонент маршрута содержит внутри себя несколько других маршрутов.


Основы маршрутизации в Haunted

Haunted не предоставляет встроенной маршрутизации, но хорошо сочетается с популярными решениями вроде vaadin-router или wouter. Вложенные маршруты обычно строятся с использованием дочерних <router-outlet> компонентов или аналогичных контейнеров, которые динамически меняют содержимое в зависимости от состояния маршрута.

Пример базового подхода:

import { html, component } from 'haunted';
import { Router } from '@vaadin/router';

function app() {
  return html`
    <header>
      <a href="/">Главная</a>
      <a href="/dashboard">Панель</a>
    </header>
    <main id="outlet"></main>
  `;
}

customElements.define('my-app', component(app));

Настройка маршрутов:

const router = new Router(document.getElementById('outlet'));
router.setRoutes([
  { path: '/', component: 'home-page' },
  { path: '/dashboard/*', component: 'dashboard-page' },
]);

Вложенные маршруты в компонентах

Ключевая идея вложенных маршрутов — внутри родительского компонента создаётся собственный роутер, который управляет отображением дочерних компонентов.

Пример компонента с вложенными маршрутами:

function dashboardPage() {
  const outletRef = document.createElement('div');
  outletRef.id = 'dashboard-outlet';

  haunted.useEffect(() => {
    const nestedRouter = new Router(outletRef);
    nestedRouter.setRoutes([
      { path: '/dashboard/overview', component: 'dashboard-overview' },
      { path: '/dashboard/settings', component: 'dashboard-settings' },
    ]);
  }, []);

  return html`
    <nav>
      <a href="/dashboard/overview">Обзор</a>
      <a href="/dashboard/settings">Настройки</a>
    </nav>
    <div id="dashboard-outlet"></div>
  `;
}

customElements.define('dashboard-page', component(dashboardPage));

Особенности:

  • useEffect позволяет инициализировать роутер только после того, как элемент появился в DOM.
  • Путь родителя (/dashboard/*) должен заканчиваться звездочкой *, чтобы маршруты дочернего уровня корректно обрабатывались.
  • Дочерние маршруты полностью изолированы и могут иметь собственную навигацию и состояния.

Управление состоянием при вложенной маршрутизации

Часто необходимо, чтобы родительский компонент мог передавать данные в дочерние маршруты. Это достигается через состояния Haunted:

import { useState } from 'haunted';

function dashboardPage() {
  const [user, setUser] = useState({ name: 'Alex' });
  const outletRef = document.createElement('div');

  haunted.useEffect(() => {
    const nestedRouter = new Router(outletRef);
    nestedRouter.setRoutes([
      {
        path: '/dashboard/overview',
        component: 'dashboard-overview',
        action: (_context, _commands) => {
          outletRef.querySelector('dashboard-overview')?.setUser(user);
        }
      },
      { path: '/dashboard/settings', component: 'dashboard-settings' },
    ]);
  }, []);

  return html`
    <nav>
      <a href="/dashboard/overview">Обзор</a>
      <a href="/dashboard/settings">Настройки</a>
    </nav>
    <div id="dashboard-outlet"></div>
  `;
}

В дочернем компоненте:

function dashboardOverview() {
  let user;

  const setUser = (newUser) => {
    user = newUser;
    haunted.render(thisTemplate(), this);
  };

  const thisTemplate = () => html`<p>Привет, ${user.name}</p>`;

  return thisTemplate();
}

customElements.define('dashboard-overview', component(dashboardOverview));

Таким образом можно управлять состоянием на уровне родителя, при этом дочерние компоненты остаются реактивными и поддерживают собственные обновления.


Динамические вложенные маршруты

Для сложных приложений часто требуется динамически создавать маршруты, например, на основе данных с сервера. В Haunted это реализуется через динамическую генерацию конфигурации маршрутов:

const nestedRoutes = fetchedData.map(item => ({
  path: `/dashboard/item/${item.id}`,
  component: 'dashboard-item',
  data: item
}));

const nestedRouter = new Router(document.getElementById('dashboard-outlet'));
nestedRouter.setRoutes(nestedRoutes);

В дочернем компоненте можно получать данные через атрибуты:

function dashboardItem() {
  let itemData;

  haunted.useEffect(() => {
    itemData = this.getAttribute('data-item');
  }, []);

  return html`<p>Элемент: ${itemData.name}</p>`;
}

customElements.define('dashboard-item', component(dashboardItem));

Рекомендации по организации вложенных маршрутов

  • Изолировать маршрутизаторы: каждый уровень вложенности должен управлять своим маршрутом, чтобы не создавать конфликтов путей.
  • Использовать useEffect для инициализации роутера: предотвращает ошибки при рендеринге до появления элемента в DOM.
  • Состояния родителя передавать через props или методы: это упрощает синхронизацию данных между уровнями.
  • Заканчивать путь родителя *: это важно для корректной работы дочерних маршрутов.
  • Минимизировать повторную инициализацию роутера: пересоздание роутера при каждом рендере может приводить к потере состояния и некорректной навигации.

Вложенные маршруты позволяют строить сложные, многоуровневые интерфейсы с полностью реактивными компонентами, сохраняя контроль над состоянием и навигацией на каждом уровне. Правильная организация роутеров и управление состоянием делают архитектуру приложения прозрачной и удобной для масштабирования.