Управление состоянием через роутер

Universal Router в JavaScript выходит за рамки простой маршрутизации URL и становится центральным механизмом управления состоянием приложения. В отличие от классических подходов, где состояние хранится отдельно (например, в Redux или локальных стореджах), роутер позволяет синхронизировать состояние с адресной строкой и навигацией.

Ключевая идея — представление состояния как функции текущего маршрута. Это обеспечивает:

  • воспроизводимость состояния через URL
  • упрощённую навигацию (назад/вперёд)
  • устранение рассинхронизации между UI и состоянием

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

Маршруты в Universal Router описываются как дерево конфигураций. Каждый узел может возвращать данные, которые фактически становятся частью состояния приложения.

Пример структуры маршрутов:

const routes = [
  {
    path: '/',
    action: () => ({ page: 'home' })
  },
  {
    path: '/users',
    action: () => ({ page: 'users' }),
    children: [
      {
        path: '/:id',
        action: ({ params }) => ({
          page: 'user',
          userId: params.id
        })
      }
    ]
  }
];

В этом примере:

  • состояние page определяется маршрутом
  • параметр userId извлекается из URL
  • итоговое состояние формируется динамически

Агрегация состояния при вложенных маршрутах

Universal Router поддерживает вложенные маршруты, что позволяет постепенно наращивать состояние.

При переходе на /users/42:

  1. Выполняется action родительского маршрута /users
  2. Затем выполняется action дочернего маршрута /:id
  3. Результаты объединяются

Механизм объединения может быть реализован вручную:

async function resolveRoute(context) {
  const result = await router.resolve(context);

  return {
    ...context.state,
    ...result
  };
}

Это позволяет:

  • формировать композиционное состояние
  • переиспользовать родительские данные
  • избегать дублирования логики

Контекст как источник состояния

Контекст (context) передаётся в каждый action и играет ключевую роль в управлении состоянием.

Пример:

const router = new UniversalRouter(routes);

router.resolve({
  pathname: '/users/42',
  state: {
    auth: { user: 'admin' }
  }
});

Внутри маршрута:

action: ({ context }) => {
  return {
    isAdmin: context.state.auth.user === 'admin'
  };
}

Контекст позволяет:

  • передавать глобальные данные
  • внедрять зависимости (API, сервисы)
  • хранить промежуточное состояние

Асинхронное состояние и загрузка данных

Одним из ключевых преимуществ Universal Router является поддержка асинхронных action.

{
  path: '/users/:id',
  async action({ params }) {
    const user = await fetch(`/api/users/${params.id}`).then(r => r.json());

    return {
      page: 'user',
      user
    };
  }
}

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

  • роутер ожидает завершения промиса
  • состояние формируется после загрузки данных
  • отсутствует необходимость в дополнительных middleware

Кэширование состояния на уровне маршрутов

Для оптимизации производительности используется кэширование результатов маршрутов.

Простейшая реализация:

const cache = new Map();

async function cachedAction(key, fn) {
  if (cache.has(key)) {
    return cache.get(key);
  }

  const result = await fn();
  cache.set(key, result);

  return result;
}

Применение:

{
  path: '/users/:id',
  action: ({ params }) =>
    cachedAction(params.id, async () => {
      const user = await fetch(`/api/users/${params.id}`).then(r => r.json());
      return { user };
    })
}

Преимущества:

  • сокращение повторных запросов
  • ускорение навигации
  • устойчивость к сетевым задержкам

Управление побочными эффектами

Хотя action должен возвращать состояние, он также может инициировать побочные эффекты:

  • логирование
  • аналитика
  • редиректы

Пример редиректа:

{
  path: '/admin',
  action: ({ context }) => {
    if (!context.state.auth?.isAdmin) {
      return { redirect: '/login' };
    }

    return { page: 'admin' };
  }
}

Обработка:

const result = await router.resolve(context);

if (result.redirect) {
  navigate(result.redirect);
}

Синхронизация состояния с URL

Одно из главных преимуществ — прозрачная синхронизация состояния и URL.

Пример:

/products?category=books&page=2

Обработка:

{
  path: '/products',
  action: ({ query }) => ({
    page: 'products',
    category: query.category,
    currentPage: Number(query.page || 1)
  })
}

Результат:

  • состояние полностью отражено в URL
  • возможно сохранение и шаринг ссылок
  • браузерная навигация работает “из коробки”

Интеграция с UI-фреймворками

Universal Router не зависит от конкретного UI, но легко интегрируется с React, Vue или другими библиотеками.

Пример с React:

function renderApp(state) {
  ReactDOM.render(<App {...state} />, document.getElementById('root'));
}

async function navigate(path) {
  const state = await router.resolve({ pathname: path });
  renderApp(state);
}

Таким образом:

  • роутер становится источником истины
  • UI просто отображает состояние
  • отсутствует дублирование логики

Разделение состояния: глобальное и маршрутное

Практика показывает эффективность разделения состояния:

Глобальное:

  • авторизация
  • настройки
  • тема

Маршрутное:

  • текущая страница
  • данные страницы
  • параметры

Пример:

const context = {
  pathname: '/users/42',
  state: {
    auth: { user: 'admin' },
    theme: 'dark'
  }
};

Маршрут добавляет:

{
  page: 'user',
  userId: 42
}

Итоговое состояние:

{
  auth: { user: 'admin' },
  theme: 'dark',
  page: 'user',
  userId: 42
}

Переходы и обновление состояния

Навигация инициирует пересчёт состояния:

window.addEventListener('popstate', async () => {
  const state = await router.resolve({
    pathname: location.pathname
  });

  renderApp(state);
});

При программной навигации:

async function goTo(path) {
  history.pushState({}, '', path);

  const state = await router.resolve({ pathname: path });
  renderApp(state);
}

Middleware-подход к управлению состоянием

Universal Router поддерживает цепочки middleware через последовательное выполнение маршрутов.

Можно внедрять промежуточную обработку:

async function withLogging(context, next) {
  console.log('Before:', context.pathname);
  const result = await next();
  console.log('After:', result);
  return result;
}

Использование:

router.resolve({
  pathname: '/users',
  middleware: [withLogging]
});

Обработка ошибок как часть состояния

Ошибки можно интегрировать в состояние:

{
  path: '/users/:id',
  async action({ params }) {
    try {
      const user = await fetch(`/api/users/${params.id}`).then(r => r.json());
      return { user };
    } catch (error) {
      return {
        error: true,
        message: 'Не удалось загрузить пользователя'
      };
    }
  }
}

UI затем реагирует на состояние:

if (state.error) {
  return <Error message={state.message} />;
}

Ленивая инициализация состояния

Маршруты могут загружаться динамически:

{
  path: '/admin',
  async action() {
    const module = await import('./admin.js');
    return module.getState();
  }
}

Это позволяет:

  • уменьшить initial bundle
  • загружать состояние по требованию
  • масштабировать приложение

Управление сложным состоянием через композицию маршрутов

Сложные интерфейсы разбиваются на независимые части:

{
  path: '/dashboard',
  children: [
    {
      path: '/stats',
      action: () => ({ stats: true })
    },
    {
      path: '/activity',
      action: () => ({ activity: true })
    }
  ]
}

Каждый маршрут отвечает за свой фрагмент состояния, а итог собирается автоматически.


Иммутабельность и чистота состояния

Рекомендуется:

  • не мутировать context.state
  • возвращать новые объекты
  • избегать скрытых изменений

Правильный подход:

return {
  ...context.state,
  newField: value
};

Паттерн “роутер как стор”

Universal Router может полностью заменить state-менеджер:

  • маршруты = редьюсеры
  • URL = источник действий
  • resolve = dispatch

Это упрощает архитектуру:

  • меньше зависимостей
  • единый источник данных
  • предсказуемое поведение

Отладка состояния

Для анализа состояния удобно логировать переходы:

async function debugResolve(path) {
  const state = await router.resolve({ pathname: path });
  console.log('State:', state);
}

Также полезно:

  • сериализовать состояние
  • сохранять snapshot
  • сравнивать изменения

Расширяемость

Universal Router легко расширяется:

  • добавление middleware
  • внедрение DI-контейнеров
  • интеграция с SSR

Пример SSR:

const state = await router.resolve({ pathname: req.url });

const html = renderToString(<App {...state} />);

Состояние формируется до рендера, что делает серверный вывод полностью детерминированным.