Полное руководство по React Router v6. Часть 2 - Продвинутые определения маршрутов
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, вместо этого просто передаются в виде пар ключ-значение объекта.Вопросы и ответы React собеседования 2023 - Часть 2
2 года назад·6 мин. на чтение
Актуальный список вопросов и ответов по ReactJS на интервью 2023 - Часть 2
- Вопросы и ответы ReactJS собеседования 2023 - Часть 1
- Вопросы и ответы ReactJS собеседования 2023 - Часть 2 (эта статья)
- Вопросы и ответы ReactJS собеседования 2023 - Часть 3
1. Что такое JSX?
- JSX — это расширение синтаксиса для JavaScript, обладающее всеми возможностями JavaScript.
- Вы можете внедрить любое выражение JavaScript в JSX, заключив его в фигурные скобки. После компиляции выражения JSX становятся обычными объектами JavaScript.
- Это означает, что вы можете использовать JSX внутри операторов
if
и цикловfor
, назначать его переменным, принимать в качестве аргументов и возвращать из функций.
2. Каков эквивалент следующего кода с использованием React.createElement
?
Эквивалент с использованиемconst element = <h1 className="greeting">Hello, world!</h1>;
React.createElement
будет выглядеть следующим образом:
const element = React.createElement( "h1", { className: "greeting" }, "Hello, world!" );
3. Что такое Redux?
- Основная идея redux заключается в том, что все состояние приложения хранится в одном хранилище. Store (хранилище) - это просто JavaScript объект.
- Единственный способ изменить состояние — отправить действие (action) из вашего приложения, а затем написать редьюсеры для этих действий, которые изменяют состояние.
- Весь переход состояния хранится внутри редьюсеров и не должен иметь никаких побочных эффектов.
4. Что такое store в Redux?
Store — это JavaScript объект, который содержит состояние приложения. Наряду с этим он также имеет следующие обязанности:- Разрешает доступ к состоянию через
getState()
. - Позволяет обновлять состояние с помощью отправки действия
dispatch(action)
. - Регистрирует слушателей через
subscribe(listener)
. - Обрабатывает отмену регистрации слушателей с помощью функции, возвращаемой из
subscribe(listener)
.
5. Разница между action и reducer.
- Action (действие) — это простые JavaScript объекты.
- Они должны иметь тип, указывающий тип выполняемого действия.
- По сути, действия — это некоторые данные, которые отправляются из вашего приложения в хранилище.
- Редьюсер — это просто чистая функция, которая принимает предыдущее состояние и действие и возвращает обновленное состояние.
6. Для чего используется Redux Thunk?
- Redux Thunk — это промежуточное программное обеспечение (middleware), которое позволяет вам писать создателей действий (action creator), которые возвращают функцию вместо действия (action). Что такое action creators?
- Затем thunk можно использовать для задержки отправки действия, если выполняется определенное условие. Это позволяет вам обрабатывать асинхронную диспетчеризацию действий.
- Подробнее о Redux Thunk можно узнать в полном видео-курсе о Redux Thunk.
7. Напишите кастомный хук, который можно использовать для debounce’а ввода.
// Хук useDebounce const useDebounce = (value, delay) => { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const timeout = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(timeout); }; }, [value]); return debouncedValue; }; // Использование const Counter = () => { const [value, setValue] = useState(0); const lastValue = useDebounce(value, 1000); return ( <div> <p> Current Value: {value} | Debounced Value: {lastValue} </p> <button onClick={() => setValue(value + 1)}>Increment</button> </div> ); };
8. Напишите кастомный хук для копирования текста в буфер обмена.
// Хук useCopyToClipboard function useCopyToClipboard(content) { const [isCopied, setIsCopied] = useState(false); const copy = useCallback(() => { navigator.clipboard .writeText(content) .then(() => setIsCopied(true)) .then(() => setTimeout(() => setIsCopied(false), 1250)) .catch((err) => alert(err)); }, [content]); return [isCopied, copy]; }
// Использование export default function App() { const [isCopied, copy] = useCopyToClipboard("Text to copy!"); return <button onClick={copy}>{isCopied ? "Copied!" : "Copy"}</button>; }
9. Как использовать хук useId
для создания уникальных идентификаторов?
useId
не принимает никаких параметров.useId
возвращает уникальную строку идентификатора, связанную с этим конкретным вызовомuseId
в этом конкретном компоненте.
Подробности можно найти в статье Что за хук useId в React?.// Использование import { useId } from "react"; const App = () => { const id = useId(); return ( <form> <label htmlFor={`email-${id}`}>Email</label> <input type="text" id={`email-${id}`} name="email" /> <label htmlFor={`password-${id}`}>Password</label> <input type="password" id={`password-${id}`} name="password" /> </form> ); }; // Плохая практика - не стоит использовать в качестве key const id = useId(); return posts.map((post) => <article key={id}>...</article>);
10. Как проверить/валидировать пропсы в React?
Мы можем использовать пакетprop-types
Раньше, до React v15.5, это было частью самого React.
Еще один вариант - это добавить к проекту TypeScript.import PropTypes from "prop-types"; function MyComponent({ name }) { return <div>Hello, {name}</div>; } MyComponent.propTypes = { name: PropTypes.string, }; export default MyComponent;
11. Приведите практический пример компонента высшего порядка в React.
- Напишем компонент высшего порядка (HOC) для отображения загрузки, пока компонент ожидает данные.
- Больше о компонентах высшего порядка можно узнать в видео Как использовать Компоненты высшего порядка React и Паттерн Render Props в ReactJS.
// Компонент высшего порядка function WithLoading(Component) { return function WihLoadingComponent({ isLoading, ...props }) { if (!isLoading) return <Component {...props} />; return <p>Please wait, fetching your data in no time...</p>; }; } export default WithLoading;
// Использование import UserListComponent from "./UserListComponent.js"; // импорт компонента import WithLoading from "./withLoading.js"; // импорт HOC const ListWithLoading = WithLoading(UserListComponent); // обернем компонент в HOC const App = () => { const [loading, setLoading] = useState(true); const [users, setUsers] = useState([]); useEffect(() => { // запрос данных const dataFromApi = ["this is coming from API call", "don't show loader"]; // в это время загрузчик будет показан в HOC // данные получены setUsers([...dataFromApi]); setLoading(false); }, []); return <ListWithLoading isLoading={loading} users={users} />; };
12. Чем полезен хук useDeferredValue
?
useDeferredValue
— это React хук, который позволяет вам отложить обновление части пользовательского интерфейса.- По сути, это позволяет вам выполнять технику debounce с меньшим количеством кода.
- Подробный разбор хука
useDeferredValue
можно прочитать в статье Хуки useTransition и useDeferredValue в ReactJS 18.
// Использование import { useState, useDeferredValue } from "react"; // Компонент userList получает searchText для запроса списка пользователей import UserList from "./UserList.js"; export default function App() { const [searchText, setSearchText] = useState(""); // отправить searchText по умолчанию const deferredQuery = useDeferredValue(searchText); return ( <> <label> Search user: <input value={searchText} onChange={(e) => setSearchText(e.target.value)} /> </label> <div> <UserList searchText={deferredQuery} /> </div> </> ); }
13. Как отследить клик за пределами компонента React?
export default function OutsideAlerter() { const clickMeDivRef = useRef(null); useEffect(() => { const handleClickOutside = (event) => { if (!ref?.current?.contains(event.target)) { alert("You clicked outside of me!"); } }; // Добавим прослушивание событий document.addEventListener("mousedown", handleClickOutside); return () => { // Удалим прослушивание событий document.removeEventListener("mousedown", handleClickOutside); }; }, [clickMeDivRef]); return <div ref={clickMeDivRef}>Clicked me?</div>; }
14. Почему имена компонентов React должны начинаться с заглавных букв?
В JSX имена тегов нижнего регистра считаются тегами HTML. Однако имена тегов в нижнем регистре с точкой (аксессор свойства) не являются таковыми.<person />
компилируется в React.createElement('person') (тег html)компилируется в React.createElement(Person)
<obj.person />
компилируется вReact.createElement(obj.person)
// Ошибка! Это компонент и должен начинаться с заглавной буквы function person(props) { // Верно! Это использование <div> верно, т.к. div это валидный элемент return <div>{props.isLearning ? "Great!" : "Hello!"}</div>; } function App() { // Ошибка! React считает <person /> тэгом HTML, т.к. Не с заглавной буквы return <person isLearning={true} />; } // Верно! Это компонент и должен начинаться с заглавной буквы function Person(props) { // Верно! Это использование <div> верно, т.к. div это валидный элемент return <div>{props.isLearning ? "Great!" : "Hello!"}</div>; } function App() { // Верно! React знает, что <Person /> - это компонент, с заглавной буквы return <Person isLearning={true} />; }
15. В чем разница между npx и npm?
- npm — это менеджер пакетов, который можно использовать для установки пакетов node.js. npM - Manager.
- npx— это инструмент для выполнения пакетов node.js. npX - Execute.
package.json
.
Поэтому, если вы хотите быстро проверить/запустить пакет без его установки - используйте npx.
create-react-app
— это npm пакет, который должен запускаться только один раз в жизненном цикле проекта. Следовательно, предпочтительнее использовать npx для установки и запуска за один шаг.
> npx create-react-app my-app
16. Как установить фокус на поле ввода после монтирования компонента в UI?
Еще больше вопросов с собеседованийimport React, { useEffect, useRef } from "react"; const SearchPage = () => { const textInput = useRef(null); useEffect(() => { textInput.current.focus(); }, []); return ( <div> <input ref={textInput} type="text" /> </div> ); };