Перейти к содержанию

Маршруты и ссылки приложения

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

Используем declarative API выбранной версии React Router 8.4.0. Здесь маршруты описываются компонентами Routes и Route, а element содержит JSX экрана. Мы не переносим старые примеры со Switch, exact и useHistory: у нашего профиля другой интерфейс библиотеки, подтверждённый в документации маршрутизации.

Один BrowserRouter над приложением

Контекст маршрутизации должен быть доступен навигации и экранам. Поэтому BrowserRouter находится в src/main.jsx над App, внутри уже существующего StrictMode. Второй BrowserRouter в Navbar или на странице не нужен: это создало бы вложенную конкурирующую область маршрутизации вместо общего адреса приложения.

Полный src/main.jsx:

import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import App from './App.jsx';
import {BrowserRouter} from 'react-router';
import './styles.css';
createRoot(document.getElementById('root')).render(<StrictMode><BrowserRouter><App /></BrowserRouter></StrictMode>);

CreateRoot по-прежнему создаёт React-корень в существующем DOM-элементе root. Добавление маршрутизатора не меняет способ подключения приложения к HTML. BrowserRouter участвует в дереве React и связывает его с историей браузера; он не является новым сервером и не начинает обслуживать файлы по адресам.

Routes выбирает экран

В App оставляем общий Navbar и main. Внутри main находится таблица маршрутов. Home соответствует корню, CreateNote — пути /new. Сейчас CreateNote содержит заглушку: форму будем строить позже, после подробной карточки. Наличие адреса не означает, что функция создания уже готова.

Полный src/App.jsx:

import {Routes, Route} from 'react-router';
import Navbar from './components/Navbar.jsx';
import Home from './pages/Home.jsx';
import CreateNote from './pages/CreateNote.jsx';
export default function App() {
  return <><Navbar /><main><Routes>
    <Route path="/" element={<Home />} />
    <Route path="/new" element={<CreateNote />} />
  </Routes></main></>;
}

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

Адрес Элемент в Routes Состояние этого этапа
/ Home Каталог через API
/new CreateNote Подготовленная заглушка
/notes/html-start Ещё нет маршрута Добавим следующим уроком
/unknown Ещё нет fallback Добавим в конце серии

Таблица важна для ожиданий. Если открыть неизвестный путь сейчас, общий хедер сохранится, но подходящего экрана не будет. Это промежуточное ограничение исходника, а не доказательство, что обработка 404 уже работает. Не будем выдавать будущий результат за текущий.

Link сохраняет семантику ссылки

В Navbar заменяем прежний якорь на Link с to. Это навигация внутри нашего приложения, поэтому Router может изменить адрес и выбранный экран без полной загрузки нового HTML-документа. Полный файл компонента:

import {Link} from 'react-router';
export default function Navbar() {
  return <header className="site-header"><strong>Профессор Веб</strong><nav aria-label="Учебная навигация"><Link to="/">Каталог</Link><Link to="/new">Новая карточка</Link></nav></header>;
}

Link создаёт ссылку, а не кнопку, притворяющуюся навигацией. Пользователь сохраняет привычные действия со ссылками, включая открытие адреса отдельно. Для действия «Убрать из подборки» остаётся button: оно меняет выбор, а не адрес. Выбор HTML-элемента следует назначению действия.

Обычная a по-прежнему нужна для внешних сайтов, скачивания архивов и перехода из учебной SPA в отдельную статическую библиотеку. React Router не должен перехватывать каждый URL на свете. Если ссылка ведёт на существующий старый PHP-адрес ProfessorWeb, это другой документ сайта, а не обязательно экран нашего каталога.

Неправильный вариант — onClick с window.location для каждого пункта меню без реальной ссылки. Он усложняет открытие в новой вкладке и переносит навигацию в обработчик, хотя готовый Link уже выражает нужный смысл. Программный переход понадобится после успешного POST, когда адрес определяется ответом сервера.

Маршрут определяется структурой пути

Routes сопоставляет URL с заданными шаблонами. Мы не пишем условие location.pathname.includes('new'), потому что подстрока может встретиться в другом сегменте и выбрать неверный экран. Путь /new является явной частью контракта, а будущий /notes/:noteId будет выделять параметр записи.

Полезно различать pathname, query и hash. В этом уроке фильтр темы хранится в React-состоянии и не записан в строку запроса. Переход на /?topic=css не настроит наш filter автоматически. Для такого поведения пришлось бы отдельно читать параметры поиска и договориться, кто владеет фильтром.

Аналогично fragment #catalog не становится новым маршрутом сам по себе. Он обозначает часть документа. Ранее Navbar использовал такой якорь, потому что у страницы был один экран. Теперь у нас появляется другая задача — переключение экранов с отдельными путями, поэтому меняется и способ навигации.

Что случается при прямом открытии адреса

Когда пользователь нажимает Link в уже загруженном приложении, браузерная история обновляется на клиенте. Когда он вставляет /new в адресную строку или обновляет страницу, первоначальный запрос получает сервер, обслуживающий HTML. Этот сервер должен знать, какой документ вернуть для маршрута SPA.

Учебный сервер API обрабатывает только /api/notes и его операции. Он не выдаёт Vite-клиент и не настроен для production fallback. Сервер разработки Vite и отдельный будущий хостинг — разные среды. Поэтому работающее переключение экранов не доказывает, что прямые адреса уже будут правильно обслуживаться после размещения.

Общее правило будущей конфигурации: путь API нельзя маскировать выдачей index.html, иначе клиент получит HTML вместо JSON. Для старых публичных страниц тоже нужен отдельный сохранённый маршрут. Подробные границы SPA и поисковой индексации обсудим в последнем уроке; здесь мы только определяем интерфейс адресов.

Переименование ссылки в меню не меняет маршрут: текст предназначен читателю, а to остаётся частью договора адресов. Если решено заменить /new на другой путь, исправлять надо одновременно Route и все ссылки. Поэтому полезно сначала согласовать небольшой список адресов, а потом строить навигацию. Это тот же принцип стабильного идентификатора, который ранее помог сохранить связь карточки с данными.

Как связать переход с состоянием

После ухода с Home его локальные hiddenIds не обещают сохранение. При новом монтировании компонент снова создаёт пустой список скрытых записей и читает API. Это ожидаемо для текущей архитектуры. Если предпочтения должны переживать переходы, их владельца придётся поднять выше маршрутов или сохранять отдельно.

При самостоятельном запуске переход «Новая карточка» должен изменить адрес на /new и показать заглушку, а «Каталог» вернуть Home. Кнопки фильтра меняют состояние, но не URL. Эти наблюдения различают работу Router и обычное обновление компонента. Следующим шагом свяжем идентификатор в пути с запросом подробной записи.

Оглавление серии