Полное руководство по 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, вместо этого просто передаются в виде пар ключ-значение объекта.

Кастомный хук для диспатчинга Redux экшенов

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

В этой статье напишем кастомный хук для вызова action creator’а Redux вместе с dispatch() внутри.

Часто, вызывая action creator, мы забываем обернуть его в dispatch(), а вместо этого вызываем как обычная функцию. Таким образом action не доходит до редьюсера.
addProductToCart(product); // не верно
dispatch(addProductToCart(product)); // верно
Мы создадим собственный хук, в котором будет выполняться обертывание вызова функции в dispatch(), чтобы мы могли отправлять наши экшены, вызывая их как обычные функции с уже встроенным диспатчингом.
import { useCallback } from 'react';
import { useDispatch } from 'react-redux';

export const useWithDispatch = (fn) => {
  const dispatch = useDispatch();

  return useCallback(
    payload => () => dispatch(fn(payload)),
    [dispatch, fn]
  ) 
}
Реализация довольно проста. useWithDispatch — это функция высшего порядка, поскольку она принимает функцию (action creator) в качестве аргумента и возвращает другую функцию, которая при вызове вызовет переданную функцию, обернутую с помощью dispatch(). Обязательно запоминаем функцию обратного вызова с помощью useCallback.
Допустим, мы работаем над приложением списка задач, и у нас есть действие addTask(), которое берет текст задачи и добавляет его в список задач:
// actions.js

const addTaskAction = (text) => {
  return {
    type: 'ADD_TASK',
    payload: {
      id: 'some-id',
      text
    }
  }
}
В компоненте вызов будет выглядеть следующим образом.
import { addTaskAction } from './actions';

export const Component = () => {
  const addTask = useWithDispatch(addTaskAction);

  const handleClick = () => {
    addTask('Learn Redux');
  }

  // ...
}

Итоги

Я надеюсь, что вы нашли этот пост полезным. Вам по-прежнему нужно помнить о передаче создателей действий (action creator) в useWithDispatch, но будет легче запомнить, что вы делаете это в самом начале, а не при их вызове.