Полное руководство по React Router v6. Часть 2 - Продвинутые определения маршрутов

год назад·8 мин. на чтение

В этой статье рассмотрим то, как описывать роуты в React Router версии 6. Как создавать вложенные маршруты, контекст, хук useRoutes и многое другое.

Серия статей о React Router v6 состоит из 4 частей.
  1. Основы React Router
  2. Продвинутые определения маршрутов (рассматривается в этой статье)
  3. Управление навигацией
  4. Подробно о роутерах
Здесь React Router действительно становится интересным. Есть много интересных вещей, которые вы можете сделать с роутингом, чтобы сделать более сложные маршруты, более удобные для чтения и в целом более функциональные. Это можно сделать с помощью пяти основных методов.
  1. Динамическая маршрутизация
  2. Приоритет маршрутизации
  3. Вложенные маршруты
  4. Множественные маршруты
  5. Хук useRoutes

Динамическая маршрутизация

Самая простая и распространенная расширенная функция в React Router — это обработка динамических маршрутов. В нашем примере предположим, что мы хотим визуализировать компонент для отдельных книг в нашем приложении. Мы могли бы жестко закодировать каждый из этих маршрутов, но если у нас есть сотни книг или возможность для пользователей создавать книги, то невозможно жестко закодировать все эти маршруты. Вместо этого нам нужен динамический маршрут.
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books" element={<BookList />} />
  <Route path="/books/:id" element={<Book />} />
</Routes>
Последний маршрут в приведенном выше примере — это динамический маршрут с динамическим параметром :id. Для определения динамического маршрута в React Router нужно поставить двоеточие перед тем, что должно изменяться. В нашем случае наш динамический маршрут будет соответствовать любому URL-адресу, который начинается с /book и заканчивается каким-либо значением. Например, /books/1, /books/bookName и /books/literally-anything будут соответствовать нашему динамическому маршруту. Почти всегда, когда у вас есть такой динамический маршрут, вы хотите получить доступ к динамическому значению в вашем пользовательском компоненте, где на помощь приходит хук useParams.
import { useParams } from "react-router-dom"

export function Book() {
  const { id } = useParams()

  return (
    <h1>Book {id}</h1>
  )
}
Хук useParams не принимает параметры и возвращает объект с ключами, соответствующими динамическим параметрам в вашем маршруте. В нашем случае нашим динамическим параметром является :id, поэтому хук useParams вернет объект, имеющий ключ id, и значение этого ключа будет фактическим идентификатором в нашем URL. Например, если бы наш URL-адрес был /books/3, наша страница отображала бы Book 3.

Приоритет маршрутизации

Когда мы имели дело только с жестко закодированными маршрутами, было довольно легко узнать, какой маршрут будет отображаться, но при работе с динамическими маршрутами это может быть немного сложнее. Возьмем, к примеру, эти маршруты.
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books" element={<BookList />} />
  <Route path="/books/:id" element={<Book />} />
  <Route path="/books/new" element={<NewBook />} />
</Routes>
Если у нас есть URL-адрес /books/new, какой маршрут будет задействован? Технически у нас есть два маршрута, которые совпадают. И /books/:id, и /books/new. Они будут совпадать, так как динамический маршрут будет просто предполагать, что new является частью :id URL-адреса, поэтому React Router нужен другой способ определить /books/:id какой маршрут отренедрить. В старых версиях React Router маршрут, который был определен первым, будет визуализирован, поэтому в нашем случае будет отображаться маршрут /books/:id, что, очевидно, не то, что нам нужно. К счастью, версия 6 React Router изменила это подход, поэтому теперь React Router будет использовать алгоритм, чтобы определить, какой маршрут, скорее всего, является тем, который вам нужен. В нашем случае мы, очевидно, хотим отобразить /books/new, поэтому React Router выберет этот маршрут для нас. Фактический принцип работы этого алгоритма очень похож на специфику CSS, поскольку он попытается определить, какой маршрут, соответствующий нашему URL-адресу, является наиболее конкретным (имеет наименьшее количество динамических элементов), и выберет этот маршрут. Раз уж мы заговорили о приоритете маршрутизации, я также хочу поговорить о том, как создать маршрут, который соответствует всему.
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books" element={<BookList />} />
  <Route path="/books/:id" element={<Book />} />
  <Route path="/books/new" element={<NewBook />} />
  <Route path="*" element={<NotFound />} />
</Routes>
* соответствует чему угодно, что делает его идеальным для таких вещей, как страница 404. Маршрут, содержащий *, также будет менее конкретным, чем все остальное, поэтому вы никогда случайно не сопоставите маршрут *, когда другой маршрут также совпал бы.

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

В приведенном выше примере у нас есть три маршрута, которые начинаются с /books, поэтому мы можем вложить эти маршруты друг в друга, чтобы очистить наши маршруты.
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books">
    <Route index element={<BookList />} />
    <Route path=":id" element={<Book />} />
    <Route path="new" element={<NewBook />} />
  </Route>
  <Route path="*" element={<NotFound />} />
</Routes>
Это вложение довольно просто сделать. Все, что вам нужно сделать, это создать родительский Route, в котором свойство path будет установлено на общий путь для всех ваших дочерних компонентов Route. Внутри родительского маршрута вы можете поместить все дочерние компоненты Route. Единственное отличие состоит в том, что проп path дочерних компонентов Route больше не включает общий маршрут /books. Кроме того, маршрут для /books заменяется компонентом Route, который не имеет проп path, но вместо этого имеет index. Все это говорит о том, что путь индексного маршрута совпадает с родительским Route. Но истинная сила вложенных маршрутов заключается в том, как они обрабатывают общие макеты.

Общие макеты (layout)

Давайте представим, что мы хотим отобразить раздел nav со ссылками на каждую книгу, а также форму для добавления книг с любой из наших страниц. Чтобы сделать это, как правило, нам нужно было бы создать общий компонент для хранения этой навигации, а затем импортировать его в каждый отдельный компонент, связанный с книгой. Однако это создает дублирование, поэтому React Router создал собственный вариант решения этой проблемы. Если вы передадите проп element родительскому маршруту, он отобразит этот компонент для каждого дочернего Route, что означает, что вы можете легко разместить общую навигацию или другие общие компоненты на каждой дочерней странице.
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books" element={<BooksLayout />}>
    <Route index element={<BookList />} />
    <Route path=":id" element={<Book />} />
    <Route path="new" element={<NewBook />} />
  </Route>
  <Route path="*" element={<NotFound />} />
</Routes>
import { Link, Outlet } from "react-router-dom"

export function BooksLayout() {
  return (
    <>
      <nav>
        <ul>
          <li><Link to="/books/1">Book 1</Link></li>
          <li><Link to="/books/2">Book 2</Link></li>
          <li><Link to="/books/new">New Book</Link></li>
        </ul>
      </nav>

      <Outlet />
    </>
  )
}
Наш новый код будет работать следующим образом: всякий раз, когда мы сопоставляем маршрут внутри родительского Route /book, он будет отображать компонент BooksLayout, который содержит нашу общую навигацию. Route, какой бы дочерний маршрут ни был сопоставлен, он будет отображаться везде, т.к. компонент Outlet находится внутри нашего компонента макета. Компонент Outlet — это, по сути, компонент-заполнитель, который будет отображать любое содержимое нашей текущей страницы. Эта структура невероятно полезна и делает обмен кодом между маршрутами невероятно простым. Теперь последний способ, которым вы можете поделиться макетами с React Router, - это обернуть дочерние компоненты Route в родительский Route, который определяет только проп element и не имеет пропса path.
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books" element={<BooksLayout />}>
    <Route index element={<BookList />} />
    <Route path=":id" element={<Book />} />
    <Route path="new" element={<NewBook />} />
  </Route>
  <Route element={<OtherLayout />}>
    <Route path="/contact" element={<Contact />} />
    <Route path="/about" element={<About />} />
  </Route>
  <Route path="*" element={<NotFound />} />
</Routes>
Этот фрагмент кода создаст два маршрута, /contact и /about, которые отображаются внутри компонента OtherLayout. Метод оборачивания нескольких компонентов Route в родительский компонент Route без пропса path полезен, если вы хотите, чтобы эти маршруты использовали один макет, даже если у них нет похожего path.

Контекст Outlet

Еще одна важная вещь, которую нужно знать о компонентах Outlet, это то, что они могут принимать проп context, который будет работать так же, как контекст React.
import { Link, Outlet } from "react-router-dom"

export function BooksLayout() {
  return (
    <>
      <nav>
        <ul>
          <li><Link to="/books/1">Book 1</Link></li>
          <li><Link to="/books/2">Book 2</Link></li>
          <li><Link to="/books/new">New Book</Link></li>
        </ul>
      </nav>

      <Outlet context={{ hello: "world" }} />
    </>
  )
}
import { useParams, useOutletContext } from "react-router-dom"

export function Book() {
  const { id } = useParams()
  const context = useOutletContext()

  return (
    <h1>Book {id} {context.hello}</h1>
  )
}
Как видно из этого примера, мы передаем значение контекста { hello: "world" }, а затем в нашем дочернем компоненте мы используем хук useOutletContext для доступа к значению нашего контекста. Это довольно распространенный шаблон для использования, поскольку часто у вас будут общие данные между всеми вашими дочерними компонентами, что является идеальным вариантом использования для этого контекста.

Множественные маршруты

Еще одна невероятно мощная вещь, которую вы можете сделать с помощью React Router, — это использовать несколько компонентов Routes одновременно. Это может быть сделано либо в виде двух отдельных компонентов Routes, либо в виде вложенных Routes.

Отдельные Routes

Если вы хотите отобразить два разных раздела контента, которые зависят от URL-адреса приложения, вам потребуется несколько компонентов Routes. Это очень распространенный случай, если, например, у вас есть боковая панель, на которой вы хотите отображать определенный контент для определенных URL-адресов, а также главная страница, которая должна отображать определенный контент на основе URL-адреса.
import { Route, Routes, Link } from "react-router-dom"
import { Home } from "./Home"
import { BookList } from "./BookList"
import { BookSidebar } from "./BookSidebar"

export function App() {
  return (
    <>
      <nav>
        <ul>
          <li><Link to="/">Home</Link></li>
          <li><Link to="/books">Books</Link></li>
        </ul>
      </nav>

      <aside>
        <Routes>
          <Route path="/books" element={<BookSidebar />}>
        </Routes>
      </aside>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/books" element={<BookList />} />
      </Routes>
    </>
  )
}
В приведенном выше примере у нас есть два Routes. Основные маршруты Routes определяют все основные компоненты для нашей страницы, а затем у нас есть вторичные Routes внутри aside, которые будут отображать боковую панель для страницы наших книг, когда мы находимся в /books. Это означает, что если наш URL-адрес /books, то оба наших компонента Routes будут отображать контент, поскольку они оба имеют уникальное совпадение для /books в своих Routes. Еще одна вещь, которую вы можете сделать с несколькими компонентами Routes, — это жестко закодировать проп location.
<Routes location="/books">
  <Route path="/books" element={<BookSidebar />}>
</Routes>
Жестко кодируя проп location, мы переопределяем поведение по умолчанию React Router, поэтому независимо от того, каков URL-адрес нашей страницы, этот компонент Routes будет соответствовать своему Route, как если бы URL-адрес был /books.

Вложенные Routes

Другой способ использования нескольких компонентов Routes заключается в том, чтобы вложить их друг в друга. Это довольно распространено, если у вас много маршрутов и вы хотите очистить свой код, переместив похожие маршруты в свои собственные файлы.
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books/*" element={<BookRoutes />} />
  <Route path="*" element={<NotFound />} />
</Routes>
import { Routes, Route } from "react-router-dom"
import { BookList } from "./pages/BookList"
import { Book } from "./pages/Book"
import { NewBook } from "./pages/NewBook"
import { BookLayout } from "./BookLayout"

export function BookRoutes() {
  return (
    <Routes>
      <Route element={<BookLayout />}>
        <Route index element={<BookList />} />
        <Route path=":id" element={<Book />} />
        <Route path="new" element={<NewBook />} />
        <Route path="*" element={<NotFound />} />
      </Route>
    </Routes>
  )
}
Вложенные Routes в React Router довольно просты в реализации. Все, что вам нужно сделать, это создать новый компонент для хранения вложенных Routes, этот компонент должен иметь компонент Routes, а внутри этого компонента Routes должны быть все компоненты Route, которые вы сопоставляете с родительским Route. В нашем случае мы перемещаем все наши маршруты /books в этот компонент BookRoute. Затем в родительских Routes вам нужно определить Route, который имеет путь, равный пути, разделяемому всеми вашими вложенными Routes. В нашем случае это будет /books. Однако важно то, что вам нужно заканчивать path родительского маршрута маршрутом *, иначе он не будет правильно соответствовать дочерним роутам. По сути, код, который мы написали, говорит, что всякий раз, когда маршрут начинается с /book/, он должен искать внутри компонента BookRoutes, чтобы увидеть, есть ли соответствующий Route. Вот почему у нас есть еще один маршрут * в BookRoutes, чтобы мы могли гарантировать, что если наш URL-адрес не совпадает ни с одним из BookRoutes, он правильно отобразит компонент NotFound.

Хук useRoutes

Последнее, что вам нужно знать об определении маршрутов в React Router, это то, что вы можете использовать объект JavaScript для определения ваших маршрутов вместо JSX.
import { Route, Routes } from "react-router-dom"
import { Home } from "./Home"
import { BookList } from "./BookList"
import { Book } from "./Book"

export function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/books">
        <Route index element={<BookList />} />
        <Route path=":id" element={<Book />} />
      </Route>
    </Routes>
  )
}
import { Route, Routes } from "react-router-dom"
import { Home } from "./Home"
import { BookList } from "./BookList"
import { Book } from "./Book"

export function App() {
  const element = useRoutes([
    {
      path: "/",
      element: <Home />
    },
    {
      path: "/books",
      children: [
        { index: true, element: <BookList /> },
        { path: ":id", element: <Book /> }
      ]
    }
  ])

  return element
}
Эти два компонента имеют одни и те же маршруты, единственное различие заключается в том, как они были определены. Если вы все же решите, что хотите использовать хук useRoutes, все пропсы, которые вы обычно передаете компонентам Route, вместо этого просто передаются в виде пар ключ-значение объекта.

Полное руководство по React Router v6. Часть 4 - Подробно о роутерах

год назад·3 мин. на чтение

В этой статье рассмотрим все виды роутеров из библиотеки React Router v6 - BrowserRouter, NativeRouter, HashRouter, HistoryRouter, MemoryRouter, StaticRouter.

В первой части мы говорили о настройке маршрутизатора и упоминали BrowserRouter и NativeRouter, но это не единственные типы роутеров. Всего существует 6 роутеров, и в этой части мы подробно рассмотрим каждый из них. Серия статей о React Router v6 состоит из 4 частей.
  1. Основы React Router
  2. Продвинутые определения маршрутов
  3. Управление навигацией
  4. Подробно о роутерах (рассматривается в этой статье)

BrowserRouter

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

NativeRouter

NativeRouter по сути является эквивалентом BrowserRouter, но для React Native. Если вы используете React Native, то это роутер, который вы захотите использовать.

HashRouter

Этот роутер работает очень похоже на BrowserRouter, но основное отличие заключается в том, что вместо того, чтобы изменять URL-адрес на что-то вроде http://localhost:3000/books он будет хранить URL-адрес в хэше, как http://localhost:3000/#/books. Как видите, этот URL-адрес имеет # после URL-адреса, который представляет собой хэш-часть URL-адреса. Все, что находится в хэш-части URL-адреса, является просто дополнительной информацией, которая обычно обозначает идентификатор на странице для целей прокрутки, поскольку страница будет автоматически прокручиваться до элемента с идентификатором, представленным хешем, при загрузке страницы. В React Router этот хэш на самом деле не используется для хранения идентификационной информации для прокрутки, а вместо этого он хранит информацию, связанную с текущим URL-адресом. Причина, по которой React Router делает это, заключается в том, что некоторые хостинг-провайдеры не позволяют вам фактически изменять URL-адрес вашей страницы. В этих очень редких случаях вы захотите использовать HashRouter, поскольку HashRouter не изменит фактический URL-адрес вашей страницы, а изменит только хэш вашей страницы. Если вы можете использовать какой-либо URL-адрес у своего хостинг-провайдера, то это не то, что вам следует использовать.

HistoryRouter

HistoryRouter (в настоящее время называется unstable_HistoryRouter) — это роутер, который позволяет вручную управлять объектом истории, который React Router использует для хранения всей информации, связанной с историей маршрутизации вашего приложения. Этот объект истории помогает убедиться, что такие вещи, как кнопки «Назад» и «Вперед» в браузере, работают правильно. Это роутер, который вы, вероятно, никогда не должны использовать, если у вас нет очень конкретной причины, по которой вы хотите перезаписать или контролировать поведение истории по умолчанию React Router.

MemoryRouter

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

StaticRouter

Роутер StaticRouter также имеет очень специфический вариант использования. Этот маршрутизатор специально предназначен для серверного рендеринга ваших приложений React, поскольку он принимает один проп location и визуализирует ваше приложение, используя этот location в качестве URL-адреса. Этот роутер на самом деле не может выполнять какую-либо маршрутизацию и будет просто отображать одну статическую страницу, но это идеально подходит для рендеринга на сервере, поскольку вы хотите просто отобразить HTML вашего приложения на сервере, а затем клиент может настроить всю вашу маршрутизацию и так далее.
<StaticRouter location="/books">
  <App />
</StaticRouter>

Итоги

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