Полное руководство по React Router v6. Часть 2 - Продвинутые определения маршрутов
год назад·8 мин. на чтение
В этой статье рассмотрим то, как описывать роуты в React Router версии 6. Как создавать вложенные маршруты, контекст, хук useRoutes и многое другое.
Серия статей о React Router v6 состоит из 4 частей.
Наш новый код будет работать следующим образом: всякий раз, когда мы сопоставляем маршрут внутри родительского
Контекст
Еще одна важная вещь, которую нужно знать о компонентах
Отдельные
Если вы хотите отобразить два разных раздела контента, которые зависят от URL-адреса приложения, вам потребуется несколько компонентов
Вложенные
Другой способ использования нескольких компонентов
Хук
Последнее, что вам нужно знать об определении маршрутов в React Router, это то, что вы можете использовать объект JavaScript для определения ваших маршрутов вместо JSX.
- Основы React Router
- Продвинутые определения маршрутов (рассматривается в этой статье)
- Управление навигацией
- Подробно о роутерах
- Динамическая маршрутизация
- Приоритет маршрутизации
- Вложенные маршруты
- Множественные маршруты
- Хук
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
.
Приоритет маршрутизации
Когда мы имели дело только с жестко закодированными маршрутами, было довольно легко узнать, какой маршрут будет отображаться, но при работе с динамическими маршрутами это может быть немного сложнее. Возьмем, к примеру, эти маршруты.Если у нас есть URL-адрес<Routes> <Route path="/" element={<Home />} /> <Route path="/books" element={<BookList />} /> <Route path="/books/:id" element={<Book />} /> <Route path="/books/new" element={<NewBook />} /> </Routes>
/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
, но будет легче запомнить, что вы делаете это в самом начале, а не при их вызове.