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

Два Outlet для вложенных страниц каталогаSiteLayout показывает общую навигацию; CatalogLayout — заголовок каталога.SiteLayoutНавигация + OutletCatalogLayoutЗаголовок + Outlet/catalogСписок товаров/catalog/:productIdКарточка товара* → Страница 404Неизвестный путь ведёт в 404; неизвестный productId проверяем отдельно.
Список и карточка вложены в CatalogLayout. Страница 404 для неизвестного пути открывается в SiteLayout. Неизвестный productId проверяем внутри карточки.

Выберите один режим и подготовьте проект

Нужны базовый JavaScript, компоненты React и понимание JSX. Если React пока знаком только по названию, начните с объяснения работы React. Этот пример посвящён маршрутам, поэтому компоненты будем разбирать по их задаче в каталоге.

Используем Declarative Mode, то есть декларативный режим React Router. В нём пути задаются компонентами Routes и Route, а всё приложение находится внутри BrowserRouter. Для небольшого каталога с учебными данными в памяти мы рекомендуем начать с него.

Документация, проверенная 9 октября 2026 года, показывает текущую версию React Router 8.4.0. В примере устанавливаем пакет react-router этой версии. Не смешивайте его инструкции с руководствами для старых версий и с Framework Mode: у режимов разные способы запуска и загрузки данных. Установка декларативного режима.

Создайте приложение через шаблон React в Vite. Понадобятся Node.js и npm; версия Node.js должна удовлетворять требованиям выбранной версии Vite. Если команда сообщает о несовместимой среде, сначала обновите её по подсказке инструмента.

npm create vite@latest router-catalog -- --template react
cd router-catalog
npm install
npm install react-router@8.4.0
npm run dev

Терминал покажет адрес сервера разработки. Откройте его в браузере. Если приложение Vite уже есть, выполняйте установку React Router в его папке и заменяйте файлы примера там. Все данные ниже вымышлены.

Оберните приложение в BrowserRouter

Замените содержимое src/main.jsx:

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router";
import App from "./App.jsx";
import "./index.css";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </StrictMode>
);

Подключение готово. Здесь одна общая точка подключения маршрутизатора: компоненты ниже получают сведения об адресе из этой оболочки, поэтому отдельный BrowserRouter в каждой странице не нужен.

BrowserRouter использует адресную строку и историю браузера. После перехода в карточку нажмите «Назад», чтобы вернуться в список, затем «Вперёд», чтобы снова открыть товар с прежним адресом.

Соберите все страницы в одном файле

Для первого прохода держим данные и компоненты рядом. Так схема маршрутов и карточки будут перед глазами. Замените src/App.jsx следующим кодом:

import {
  Link,
  NavLink,
  Outlet,
  Route,
  Routes,
  useParams
} from "react-router";

const products = [
  { id: "lamp", title: "Лампа «Луч»", price: 2400 },
  { id: "chair", title: "Стул «Опора»", price: 6800 }
];

function SiteLayout() {
  return (
    <>
      <header>
        <nav aria-label="Основная навигация">
          <NavLink to="/" end>Главная</NavLink>
          <NavLink to="/catalog">Каталог</NavLink>
        </nav>
      </header>
      <main>
        <Outlet />
      </main>
    </>
  );
}

function Home() {
  return (
    <section>
      <h1>Учебный магазин</h1>
      <p>В каталоге два вымышленных товара.</p>
      <Link to="/catalog">Открыть каталог</Link>
    </section>
  );
}

function CatalogLayout() {
  return (
    <section>
      <h1>Каталог</h1>
      <Outlet />
    </section>
  );
}

function CatalogList() {
  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          <Link to={product.id}>{product.title}</Link>
        </li>
      ))}
    </ul>
  );
}

function ProductDetails() {
  const { productId } = useParams();
  const product = products.find((item) => item.id === productId);

  if (!product) {
    return (
      <section>
        <h2>Товар не найден</h2>
        <p>Такого идентификатора нет в учебном каталоге.</p>
        <Link to="/catalog">Вернуться к списку</Link>
      </section>
    );
  }

  return (
    <article>
      <h2>{product.title}</h2>
      <p>Цена: {product.price} ₽</p>
      <Link to="/catalog">Вернуться к списку</Link>
    </article>
  );
}

function NotFound() {
  return (
    <section>
      <h1>Страница не найдена</h1>
      <p>Проверьте адрес или откройте главную страницу.</p>
      <Link to="/">На главную</Link>
    </section>
  );
}

export default function App() {
  return (
    <Routes>
      <Route path="/" element={<SiteLayout />}>
        <Route index element={<Home />} />
        <Route path="catalog" element={<CatalogLayout />}>
          <Route index element={<CatalogList />} />
          <Route path=":productId" element={<ProductDetails />} />
        </Route>
        <Route path="*" element={<NotFound />} />
      </Route>
    </Routes>
  );
}

Сохраните файл и откройте главную страницу. На ней должна быть ссылка в каталог. В каталоге появляются два товара, а нажатие на лампу меняет адрес на /catalog/lamp и показывает её цену.

Чтобы стандартные стили шаблона не мешали проверке, замените src/index.css:

body {
  margin: 0;
  color: #222;
  background: #fff;
  font: 18px/1.5 system-ui, sans-serif;
}

header,
main {
  max-width: 60rem;
  margin-inline: auto;
  padding: 1rem;
}

nav {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
}

nav a[aria-current="page"] {
  font-weight: 700;
}

a:focus-visible {
  outline: 3px solid #2456d8;
  outline-offset: 3px;
}

Визуальная часть намеренно короткая. Результат упражнения проверяют адреса и содержимое страниц. Оформление карточек можно добавить после этого.

Проследите путь от адреса до Outlet

В конце App.jsx находится схема всех путей. Корневой маршрут показывает SiteLayout: меню и область main остаются общей оболочкой. Outlet внутри неё показывает подходящую дочернюю страницу.

Индексный маршрут с index не добавляет сегмент адреса. У корня он показывает Home на /, а внутри каталога — CatalogList на /catalog. Это начальная страница родительского пути. Вложенные и индексные маршруты.

У каталога свой Outlet. Именно туда попадает либо список, либо карточка товара. Удалите этот элемент на минуту: адрес будет меняться, но дочернее содержимое исчезнет. Верните Outlet.

path="catalog" вложен в корень, а path=":productId" — в каталог. Родительский сегмент включается в полный путь, поэтому вместе они дают /catalog/:productId. Двоеточие отмечает динамический сегмент: вместо него может стоять lamp или chair.

Прочитайте параметр и обработайте отсутствие товара

useParams() возвращает значения из совпавшего адреса. Мы берём productId и сравниваем его со строковыми идентификаторами товаров. Имена должны совпадать: :productId в маршруте и productId в компоненте.

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

Путь /catalog/unknown подходит под шаблон маршрута карточки. Поэтому звёздочка в общей схеме его не перехватит: компонент уже выбран. Отсутствие товара проверяет сам ProductDetails и показывает ссылку к списку.

А путь /contacts не подходит ни под главную страницу, ни под каталог. Его покажет NotFound. Это разные причины ошибки, и у них разные сообщения.

Сообщение «Страница не найдена» в интерфейсе не устанавливает HTTP-статус 404. За статус ответа документа отвечает сервер. Когда будете проектировать публичный магазин, отдельно решите, как сервер и приложение сообщают о неизвестных ресурсах.

Используйте ссылки для переходов

Link ведёт на другую страницу внутри приложения. В списке to={product.id} — относительный путь от текущего маршрута каталога. В ссылке возврата используем абсолютный /catalog, чтобы цель не зависела от вложенности карточки.

NavLink подходит для навигации с обозначением текущего раздела. Атрибут end у главной ссылки ограничивает совпадение концом пути: главная не должна отмечаться активной на каждой странице. В каталоге этот атрибут не задан, поэтому раздел остаётся активным и на карточке товара. Link и NavLink в декларативном режиме.

Ссылки сохраняют привычные действия браузера: переход с клавиатуры и открытие адреса в новой вкладке. Для обычного перехода в каталог не требуется обработчик кнопки с вызовом навигации.

Отделите ошибку маршрута от ошибки хостинга

На сервере разработки прямой адрес обычно открывается через HTML приложения. После размещения сборки на хостинге поведение может отличаться. Если обновление /catalog/lamp показывает серверную ошибку, проверьте настройку отдачи index.html для адресов клиентского приложения.

Это fallback, или резервная отдача HTML для клиентских путей. Настройка зависит от хостинга. Правило должно сохранять отдельную обработку реальных файлов и API, если они есть. В официальном руководстве по размещению SPA описано это общее условие; само руководство посвящено Framework Mode, конфигурацию этого режима в наш пример переносить не нужно.

Route path="*" начинает работать после загрузки приложения. Если сервер до этого не отдал HTML, React Router ещё не выполняется. Ищите причину на нужном уровне.

Проверьте маршруты по таблице

Действие или адресОжидаемый результат
Открыть /Главная и общее меню.
Перейти на /catalogСписок двух товаров.
Нажать на лампуАдрес /catalog/lamp, название и цена лампы.
Открыть /catalog/chairКарточка стула в оболочке каталога.
Открыть /catalog/unknownСообщение об отсутствующем товаре.
Открыть /contactsОбщая страница неизвестного пути.
Нажать «Назад», затем «Вперёд»Восстановить предыдущий и следующий адреса.
Обновить карточку после размещенияHTML приложения отдаётся по прямому адресу.

Теперь расширьте каталог. Для самостоятельной задачи добавьте товар desk и вложенный маршрут about внутри каталога. Страница о каталоге должна открываться на /catalog/about, а товар — на /catalog/desk. Проверьте, что конкретный маршрут about показывает своё содержимое.

Продолжите каталог после проверки JavaScript

Сначала добавьте собственную карточку и пройдите таблицу адресов. Затем сверяйте входные требования курса «React. Разработка сложных клиентских приложений». Перед переходом полезно уверенно работать с массивами, функциями и состоянием компонентов: маршрутизация связывает эти навыки в одно приложение.