Маршрутизация в React Router: как она работает и почему ее выбирают разработчики

Читать в полной версии →

React Router — решение для переключения и маршрутизации страниц React. Библиотека появилась еще в 2014 году и стала одним из первых популярных проектов с открытым исходным кодом на основе React. Рассказываем про ключевую концепцию библиотеки и разбираем, почему разработчики выбирают именно ее для организации маршрутизации. Бонусом — напишем небольшое приложение c использованием хуков useLocation и useNavigate, и увидим, как на практике работает маршрутизация через React Router.

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

Декларативная маршрутизация

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

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

Что делает React Router лучшим по сравнению с другими библиотеками маршрутизации:

Текущая версия React Router v6 внесла ряд ключевых изменений по сравнению с предыдущей версией v5:

В нашей статье все примеры будут построены c использованием React Router именно шестой версии.

Подготовка окружения

Прежде чем мы приступим к практике, необходимо подготовить рабочее окружение.

Для создания веб-приложений нам нужен react-router-dom, который включает в себя все, что есть в react-router, и добавляет несколько специфичных для DOM API компонентов, включая <BrowserRouter> и <Link>.

Создадим проект с помощью Create React App и установим react-router-dom:

npx create-react-app my-app
cd my-app
npm i react-router-dom

Кроме того, установим lorem-ipsum для генерации текста-заполнителя lorem ipsum для страниц.

npm i lorem-ipsum

Теперь пакеты react-router-dom и lorem-ipsum можно увидеть в package.json в качестве зависимостей.

Настройка маршрутов

Router — это компонент верхнего уровня с отслеживанием состояния, который заставляет работать все остальные компоненты навигации и хуки. В React Router есть BrowserRouter, HashRouter, StaticRouter, NativeRouter и MemoryRouter. Для веб-приложений обычно используется BrowserRouter. Приложение должно иметь один <BrowserRouter>, который обертывает один или несколько <Routes>.

<Routes> проверяет все свои дочерние элементы <Route>, чтобы найти наилучшее соответствие, и отображает эту часть пользовательского интерфейса.

<Route> можно определить либо как объект, либо элемент Route. Если это объект, он имеет форму { path, element }. Если это элемент Route, компонент имеет вид <Route path element>. Когда указанный путь path соответствует текущему URL-адресу, то отображается компонент, указанный в качестве элемента element. В нашем приложении мы будем использовать именно element.

Подготовим несколько страниц, для которых будем настраивать маршрутизацию:

// src/Components/Pages.jsx
import { loremIpsum } from 'lorem-ipsum';

const BuildPage = (index) => (
  <>
    <h3>Page {index}</h3>
    <div>
      Page {index} content: { loremIpsum({ count: 5 })}
    </div>
  </>
);

export const PageOne = () => BuildPage(1);
export const PageTwo = () => BuildPage(2);

В src/App.js создадим два маршрута:

// App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { PageOne, PageTwo } from './Components/Pages';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="one" element={<PageOne />} />
        <Route path="two" element={<PageTwo />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

В этом коде <BrowserRouter> и <Routes> используются для определения маршрутизатора.

В приложении есть два <Route>. Когда URL-адрес соответствует пути one, приложение показывает компонент PageOne. Когда URL-адрес соответствует пути two, приложение показывает компонент PageTwo.

Запустим приложение, выполнив команду npm start.

http://localhost:3000/one показывает PageOne

http://localhost:3000/two показывает PageTwo

Приложение работает для путей one и two. Однако http://localhost:3000 ничего не показывает, как и любые недействительные URL-адреса, такие как http://localhost:3000/anything.

Эту проблему можно решить с помощью подстановочного пути:

// App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { PageOne, PageTwo } from './Components/Pages';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        {/* подстановочный путь */}
        <Route path="*" element={<PageOne />} />
        <Route path="two" element={<PageTwo />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

Теперь http://localhost:3000/two показывает PageTwo. Во всех остальных случаях будет отображаться PageOne. Порядок указания маршрутов не имеет значения, так как React Router 6 выбирает в первую очередь наиболее точное совпадение.

Читайте также: Наталия Давыдова, фронтенд-разработчица в «Точке»: как мое комьюнити помогает джунам найти работу

Настройка вложенных маршрутов в React Router v6

Два маршрута в приведенном выше примере работают, как мы и ожидали. Однако вводить URL-адрес в адресной строке браузера неудобно. Мы хотели бы иметь возможность навигации по ссылке, которая называется <Link>.

<Link> отображает доступный элемент <a> с реальным href, указывающим на ресурс, на который он ссылается. Клик по ссылке устанавливает URL-адрес и отслеживает историю просмотров.

Создадим главную страницу, которая будет содержать ссылки <Link> на соответствующие страницы src/MainPage.js.

// src/MainPage.js
import { Link } from 'react-router-dom';

export const MainPage = () => (
  <nav>
    <ul>
      <li>
        <Link to="/one">Page One</Link>
      </li>
      <li>
        <Link to="/two">Page Two</Link>
      </li>
    </ul>
  </nav>
);

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

import {
  BrowserRouter,
  Routes,
  Route,
} from 'react-router-dom';
import { PageOne, PageTwo } from './Components/Pages';
import MainPage from './Components/MainPage';

const App = () => {

  return (
    <BrowserRouter>
      <Routes>
          <Route path="/" element={<MainPage/> } />
          <Route path="one" element={<PageOne />} />
          <Route path="two" element={<PageTwo />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

Теперь, когда URL-адрес соответствует «/», приложение будет показывать MainPage: MainPage.

Нажав на ссылку Page One, мы перейдем на PageOne. Нажав на ссылку Page Two, мы перейдем на PageTwo.

Однако внутри PageOne или PageTwo мы не можем использовать ссылки для навигации. Чтобы решить эту проблему, создадим компонент <Outlet> в MainPage. Он позволяет отображать вложенный пользовательский интерфейс при отображении дочерних маршрутов. Таким образом, при клике на 'one' будет отображаться компонент PageOne, а при клике на 'two'PageTwo.

Это src/MainPage.js с Outlet:

import React from 'react';
import { Link, Outlet } from 'react-router-dom';

const MainPage = () => {
  return (
    <>
      <nav>
        <ul>
          <li>
            <Link to='/one'>Page One</Link>
          </li>
          <li>
            <Link to='/two'>Page Two</Link>
          </li>
        </ul>
      </nav>
      <hr />
      <Outlet />
    </>
  )
};

<Outlet> вызывает вложенные маршруты, где у каждого могут быть свои дочерние маршруты, занимающие часть URL-адреса. Вложенные маршруты обычно описывают через относительные ссылки.

Вот модифицированный src/App.jsx:

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { PageOne, PageTwo } from './Components/Pages';
import MainPage from './Components/MainPage';

const App = () => {

  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<MainPage />} >
            <Route index element={<div>No page is selected.</div> } />
            <Route path="one" element={<PageOne />} />
            <Route path="two" element={<PageTwo />} />
          </Route>
      </Routes>
    </BrowserRouter>
  );
}

http://localhost:3000/ теперь выглядит так:

А так http://localhost:3000/one:

Хук useLocation

Местоположение — это объект, который представляет местоположение URL. Он основан на объекте браузера window.location.

Хук useLocation возвращает объект текущего местоположения.

import React, { useEffect } from 'react';
import { Link, Outlet, useLocation } from 'react-router-dom';

const MainPage = () => {
  const location = useLocation();

  useEffect(() => {
    console.log('Current location is ', location);
  }, [location]);

  return (
    <>
      <nav>
        <ul>
          <li>
            <Link to="/one">Page One</Link>
          </li>
          <li>
            <Link to="/two">Page Two</Link>
          </li>
        </ul>
      </nav>
      <hr />
      <Outlet />
    </>
  );
};

В переменную location мы сохраняем местоположение, которое генерируется хуком useLocation. Внутри хука useEffect мы будем выводить текущее местоположение при каждом изменении параметра location.

Если URL-адрес http://localhost:3000/, то консоль регистрирует:

Current location is {pathname: '/', search: '', hash: '', state: null, key: 'default'}

Если URL-адрес http://localhost:3000/one, то консоль регистрирует:

Current location is {pathname: '/one', search: '', hash: '', state: null, key: 'f2114bru'}

Когда URL-адрес http://localhost:3000/anything, то тут консоль регистрирует:

Current location is {pathname: '/anything', search: '', hash: '', state: null, key: 'default'}

Хук useNavigate

Хук useNavigate возвращает функцию, которую можно использовать для программной навигации. Заменим все <Link> в нашем приложении на <button> в src/MainPage.js:

import React, { useEffect } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';

const MainPage = () => {
  const location = useLocation();
  const navigate = useNavigate();

  useEffect(() => {
    console.log('Current location is ', location);
  }, [location]);

  return (
    <>
      <nav>
        <ul>
          <li>
            <button onClick={() => navigate('one', { replace: false })}>
              Page One
            </button>
          </li>
          <li>
            <button onClick={() => navigate('two', { replace: false })}>
              Page Two
            </button>
          </li>
        </ul>
      </nav>
      <hr />
      <Outlet />
    </>
  )
};

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

Хук useNavigate может принимать:

  1. Либо значение To с необязательным вторым аргументом { replace, state }, как это работает и в <Link to>
  2. Либо дельта-число, чтобы войти в стек истории. Например, навигация (-1) по своей сути аналогична нажатию кнопки «Назад».

Другие хуки

Мы показали, как использовать useLocation и useNavigate. При этом в библиотеке есть и другие хуки — например, useParams или useRoutes. С ними можно ознакомиться в официальной документации React Router.

Статья является адаптированным переводом команды Хекслета материалов из следующих источников: 1 и 2.