Кастомная реализация роутинга

React Router предоставляет готовый инструмент для маршрутизации, но понимание того, как работает роутинг на уровне компонентов, позволяет создавать собственные решения, более гибкие или оптимизированные под конкретные задачи. Кастомная реализация роутинга строится на управлении состоянием текущего пути, прослушивании изменений URL и условном рендеринге компонентов.


Управление текущим путем

Для начала необходимо хранить состояние текущего маршрута. Это можно сделать с помощью хука useState:

import { useState, useEffect } from 'react';

function useCurrentPath() {
  const [path, setPath] = useState(window.location.pathname);

  useEffect(() => {
    const onLocationCha nge = () => setPath(window.location.pathname);
    window.addEventListener('popstate', onLocationChange);

    return () => window.removeEventListener('popstate', onLocationChange);
  }, []);

  return path;
}

Ключевой момент: popstate срабатывает при навигации через кнопки «назад» и «вперед» браузера, что позволяет синхронизировать состояние с URL.


Компонент маршрута

Основная идея — условный рендеринг компонентов на основе текущего пути. Создаётся компонент Route:

function Route({ path, element }) {
  const currentPath = useCurrentPath();
  return currentPath === path ? element : null;
}

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


Поддержка динамических параметров

Для обработки динамических сегментов URL (/user/:id) применяется разбор строки пути с использованием регулярных выражений:

function matchPath(routePath, currentPath) {
  const paramNames = [];
  const regexPath = routePath.replace(/:([^\/]+)/g, (_, key) => {
    paramNames.push(key);
    return '([^\\/]+)';
  });
  const match = currentPath.match(new RegExp(`^${regexPath}$`));
  if (!match) return null;

  const params = paramNames.reduce((acc, name, index) => {
    acc[name] = match[index + 1];
    return acc;
  }, {});

  return params;
}

Компонент Route можно расширить для передачи параметров в рендеримый элемент:

function Route({ path, element }) {
  const currentPath = useCurrentPath();
  const params = matchPath(path, currentPath);

  return params ? React.cloneElement(element, { params }) : null;
}

Навигация между маршрутами

Для управления переходами создается компонент Link, который предотвращает полное перезагружение страницы:

function Link({ to, children }) {
  const handleClick = (e) => {
    e.preventDefault();
    window.history.pushState({}, '', to);
    const navEvent = new PopStateEvent('popstate');
    window.dispatchEvent(navEvent);
  };

  return <a href={to} onCl ick={handleClick}>{children}</a>;
}

Использование window.history.pushState позволяет менять URL без перезагрузки страницы, а popstate инициирует обновление состояния маршрута.


Главный компонент роутера

Для централизованного управления маршрутами можно создать компонент Router:

function Router({ children }) {
  const currentPath = useCurrentPath();

  return React.Children.map(children, child => {
    if (!React.isValidElement(child)) return null;
    const { path } = child.props;
    const params = matchPath(path, currentPath);
    return params ? React.cloneElement(child, { params }) : null;
  });
}

Такой подход позволяет вкладывать несколько Route внутри Router и управлять отображением компонентов в зависимости от пути.


Поддержка вложенных маршрутов

Вложенные маршруты реализуются через передачу дочерних элементов в Route:

function Route({ path, element, children }) {
  const currentPath = useCurrentPath();
  const params = matchPath(path, currentPath);

  if (!params) return null;

  return React.cloneElement(element, { params, children });
}

Маршрут, содержащий дочерние Route, может рендерить их внутри основного компонента, сохраняя контекст параметров и путь.


Переходы программно

Иногда необходимо инициировать навигацию из кода, без клика по ссылке. Для этого создаётся вспомогательная функция:

function navigate(to) {
  window.history.pushState({}, '', to);
  const navEvent = new PopStateEvent('popstate');
  window.dispatchEvent(navEvent);
}

Такой подход позволяет программно менять маршрут, например, после успешной отправки формы.


Отложенная загрузка компонентов

Кастомный роутинг поддерживает ленивую загрузку компонентов с помощью React.lazy и Suspense:

const UserPage = React.lazy(() => import('./UserPage'));

<Route path="/user/:id" element={
  <React.Suspense fallback={<div>Загрузка...</div>}>
    <UserPage />
  </React.Suspense>
} />

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


Поддержка NotFound маршрута

Для отображения компонента при отсутствии совпадений добавляется специальный маршрут с условием:

function NotFound({ element }) {
  const currentPath = useCurrentPath();
  const matched = false; // Логика проверки всех маршрутов

  return !matched ? element : null;
}

В реальной реализации проверка проходит по всем маршрутам внутри Router, и если ни один не совпадает, рендерится NotFound.


Итоговые принципы

  • Состояние текущего пути синхронизируется с window.location.pathname.
  • Route рендерится условно по совпадению пути, с поддержкой параметров.
  • Link и функция navigate позволяют изменять URL без перезагрузки.
  • Вложенные маршруты и ленивый рендер компонентов повышают гибкость.
  • Простая архитектура можно расширять для поддержки редиректов, защиты маршрутов и динамического построения меню.

Эта структура формирует фундамент для создания собственного роутинга, полностью управляемого React, без зависимостей от сторонних библиотек.