Миграция с React Router

React Router долгое время являлся стандартом де-факто для клиентской маршрутизации в React-приложениях. Однако с появлением более универсальных решений, таких как Universal Router, возникла необходимость пересмотра архитектуры маршрутизации.

Ключевые ограничения React Router:

  • Жёсткая привязка к React
  • Ограниченная гибкость при работе вне браузера
  • Сложности с изоморфным (SSR) рендерингом
  • Избыточность для простых сценариев
  • Ограниченный контроль над процессом разрешения маршрутов

Universal Router предлагает:

  • Независимость от фреймворка
  • Единый механизм маршрутизации для клиента и сервера
  • Полный контроль над логикой переходов
  • Простую и декларативную структуру маршрутов
  • Поддержку асинхронных действий на уровне маршрута

Концептуальные различия

React Router

  • Основан на декларативных компонентах (<Route>, <Switch>)
  • Маршруты определяются внутри JSX
  • Управление навигацией через hooks (useNavigate, useParams)
  • Жизненный цикл зависит от React

Universal Router

  • Маршруты описываются как обычные JavaScript-объекты
  • Нет зависимости от UI-библиотеки
  • Навигация управляется вручную
  • Асинхронная обработка маршрутов встроена

Структура маршрутов

React Router

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/users" element={<Users />} />
  <Route path="/users/:id" element={<UserProfile />} />
</Routes>

Universal Router

const routes = [
  {
    path: '/',
    action: () => renderHome()
  },
  {
    path: '/users',
    action: () => renderUsers()
  },
  {
    path: '/users/:id',
    action: ({ params }) => renderUserProfile(params.id)
  }
];

Ключевое отличие: вместо компонентов используются функции action, которые полностью контролируют поведение.


Инициализация роутера

React Router

Роутер инициализируется через компонент:

<BrowserRouter>
  <App />
</BrowserRouter>

Universal Router

import UniversalRouter from 'universal-router';

const router = new UniversalRouter(routes);

Дополнительно можно передать контекст:

const router = new UniversalRouter(routes, {
  context: { user: currentUser }
});

Навигация

React Router

const navigate = useNavigate();
navigate('/users/1');

Universal Router

router.resolve('/users/1');

Асинхронный характер:

await router.resolve('/users/1');

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

  • Загружать данные перед рендерингом
  • Выполнять проверки доступа
  • Управлять редиректами

Работа с параметрами

React Router

const { id } = useParams();

Universal Router

{
  path: '/users/:id',
  action: ({ params }) => {
    return renderUser(params.id);
  }
}

Параметры передаются напрямую в action.


Асинхронные маршруты

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

{
  path: '/users/:id',
  async action({ params }) {
    const user = await fetchUser(params.id);
    return renderUser(user);
  }
}

В React Router асинхронность обычно выносится в компоненты или loaders (в новых версиях), но контроль менее централизован.


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

React Router

<Route path="/users" element={<Users />}>
  <Route path=":id" element={<UserProfile />} />
</Route>

Universal Router

{
  path: '/users',
  children: [
    {
      path: '/',
      action: () => renderUsers()
    },
    {
      path: '/:id',
      action: ({ params }) => renderUser(params.id)
    }
  ]
}

Вложенность реализуется через массив children.


Контекст выполнения

Universal Router позволяет передавать контекст в каждый маршрут:

const router = new UniversalRouter(routes, {
  context: {
    user: currentUser,
    api: apiClient
  }
});

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

{
  path: '/dashboard',
  action: ({ context }) => {
    if (!context.user) {
      return redirect('/login');
    }
    return renderDashboard();
  }
}

Редиректы

React Router

<Navigate to="/login" />

Universal Router

import { redirect } from 'universal-router';

{
  path: '/private',
  action: ({ context }) => {
    if (!context.user) {
      return redirect('/login');
    }
  }
}

Редирект происходит на уровне логики маршрута.


Интеграция с React

Universal Router не занимается рендерингом. Интеграция выполняется вручную:

async function navigate(path) {
  const result = await router.resolve(path);
  ReactDOM.render(result, document.getElementById('root'));
}

Маршрут возвращает React-элемент:

{
  path: '/',
  action: () => <Home />
}

Работа с историей браузера

Universal Router не управляет history напрямую. Обычно используется history пакет:

import { createBrowserHistory } from 'history';

const history = createBrowserHistory();

history.listen(({ location }) => {
  router.resolve(location.pathname);
});

Навигация:

history.push('/users');

Обработка ошибок

const router = new UniversalRouter(routes, {
  errorHandler(error) {
    console.error(error);
    return renderErrorPage(error);
  }
});

В отличие от React Router, ошибки обрабатываются централизованно.


Lazy loading

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

Позволяет:

  • Делить код на чанки
  • Загружать только нужные части приложения

Middleware-подход

Universal Router поддерживает цепочки обработки:

const routes = [
  {
    path: '',
    action: async (context, params, next) => {
      console.log('before');
      const result = await next();
      console.log('after');
      return result;
    },
    children: [
      {
        path: '/users',
        action: () => renderUsers()
      }
    ]
  }
];

Это аналог middleware:

  • логирование
  • авторизация
  • аналитика

SSR (Server-Side Rendering)

Universal Router изначально проектировался для SSR:

app.get('*', async (req, res) => {
  const html = await router.resolve(req.path);
  res.send(renderFullPage(html));
});

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

  • единый код маршрутизации
  • предзагрузка данных
  • SEO-оптимизация

Стратегия миграции

1. Вынос маршрутов из JSX

Было:

<Route path="/users" element={<Users />} />

Станет:

{
  path: '/users',
  action: () => <Users />
}

2. Централизация логики

Перенос:

  • загрузки данных
  • проверок доступа
  • редиректов

из компонентов в action.


3. Замена навигации

Было:

navigate('/users');

Станет:

router.resolve('/users');

или через history.


4. Удаление зависимости от React Router

Удаляются:

  • <Routes>
  • <Route>
  • <Link> (заменяется на собственную реализацию)

5. Интеграция history

Добавляется управление URL вручную.


Частые проблемы

Двойной рендеринг

Причина: повторный вызов resolve. Решение: централизовать обработку переходов.


Потеря состояния

React Router автоматически сохраняет состояние компонентов. В Universal Router это нужно учитывать вручную.


Отсутствие хуков

Hooks (useParams, useLocation) отсутствуют. Аналог реализуется через контекст или props.


Когда миграция оправдана

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

Когда лучше остаться на React Router

  • простое SPA
  • нет SSR
  • команда не нуждается в гибкости
  • важна скорость разработки

Архитектурные преимущества после миграции

  • Чёткое разделение логики и представления
  • Универсальность (client + server)
  • Расширяемость через middleware
  • Прозрачный контроль над навигацией
  • Упрощение тестирования маршрутов