Учебное пособие¶
Добро пожаловать в учебное пособие! Мы будем создавать небольшое, но многофункциональное приложение, позволяющее отслеживать контакты. Мы ожидаем, что это займет от 30 до 60 минут, если вы будете следить за ходом работы.
👉 Каждый раз, когда вы видите это, это означает, что вам нужно что-то сделать в приложении!
Остальное - просто для информации и более глубокого понимания. Приступим.
Установка¶
Если вы не собираетесь работать с собственным приложением, этот раздел можно пропустить.
В этом руководстве мы будем использовать Vite для нашего бандлера и dev-сервера. Для работы с инструментом командной строки npm
вам потребуется установленный Node.js.
👉️ Откройте терминал и загрузите новое приложение React с помощью Vite:.
1 2 3 4 5 |
|
Вы должны иметь возможность посетить URL, выведенный в терминале:
1 2 3 4 |
|
Для этого урока мы взяли несколько заранее написанных CSS, чтобы не отвлекаться на React Router. Не стесняйтесь судить его строго или написать свой собственный 😅 (Мы сделали то, чего обычно не делали в CSS, чтобы разметка в этом уроке была как можно более минимальной).
👉 Копируем/вставляем учебный CSS найденный здесь в src/index.css
В этом учебнике мы будем создавать, читать, искать, обновлять и удалять данные. Типичное веб-приложение, вероятно, будет обращаться к API на вашем веб-сервере, но мы будем использовать хранилище браузера и имитировать некоторую сетевую задержку, чтобы не отвлекаться. Весь этот код не имеет отношения к React Router, поэтому просто скопируйте и вставьте его.
👉 Копируем/вставляем модуль данных учебника найден здесь в папку src/contacts.js
.
Все, что вам нужно в папке src - это contacts.js
, main.jsx
и index.css
. Все остальное (например, App.js
, assets
и т.д.) можно удалить.
👉 Удалите неиспользуемые файлы в src/
, чтобы остались только эти:
1 2 3 |
|
Если ваше приложение запущено, оно может на мгновение взорваться, просто продолжайте работать 😋. Итак, мы готовы приступить к работе!
Добавление маршрутизатора¶
Первым делом создадим Browser Router и настроим наш первый маршрут. Это позволит обеспечить маршрутизацию на стороне клиента для нашего веб-приложения.
Точкой входа является файл main.jsx
. Откройте его, и мы поместим React Router на страницу.
👉 Создание и рендеринг браузерного маршрутизатора в main.jsx
src/main.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Этот первый маршрут мы часто называем "корневым", поскольку все остальные маршруты будут отрисовываться внутри него. Он будет служить корневым макетом пользовательского интерфейса, в дальнейшем мы будем использовать вложенные макеты.
Корневой маршрут¶
Добавим глобальный макет для этого приложения.
👉 Создайте папки src/routes
и src/routes/root.jsx
.
1 2 |
|
(Если вы не хотите быть "занудой" в командной строке, используйте вместо этих команд редактор 🤓)
👉 Создание корневого компонента макета
src/routes/root.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
|
Ничего специфичного для React Router пока нет, так что смело копируйте/вставляйте все это.
👉 Установите <Root>
в качестве element
корневого маршрута.
src/main.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Теперь приложение должно выглядеть примерно так. Приятно иметь дизайнера, который также может написать CSS, не так ли? (Спасибо Jim 🙏).
Обработка ошибок Not Found¶
Всегда полезно знать, как ваше приложение реагирует на ошибки на ранних этапах проекта, ведь при создании нового приложения мы все пишем гораздо больше ошибок, чем возможностей! Это не только поможет вашим пользователям получить хороший опыт, но и поможет вам в процессе разработки.
Мы добавили несколько ссылок в это приложение, давайте посмотрим, что произойдет, когда мы нажмем на них?
👉 Нажмите на одно из названий боковой панели
Отвратительно! Это стандартный экран ошибок в React Router, который усугубляется нашими стилями flex box для корневого элемента в этом приложении 😂.
В любой момент, когда ваше приложение выдает ошибку при рендеринге, загрузке данных или их мутации, React Router поймает ее и выдаст экран ошибки. Давайте сделаем собственную страницу ошибок.
👉 Создать компонент страницы ошибок
1 |
|
src/error-page.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
👉 Установите <ErrorPage>
в качестве errorElement
на корневом маршруте.
src/main.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Теперь страница с ошибкой должна выглядеть следующим образом:
(Ну, это не намного лучше. Может быть, кто-то забыл попросить дизайнера сделать страницу ошибок. Может быть, все забывают попросить дизайнера сделать страницу ошибок, а потом обвиняют дизайнера в том, что он об этом не подумал 😆)
Обратите внимание, что в useRouteError
указывается ошибка, которая была выброшена. Когда пользователь переходит к несуществующим маршрутам, вы получите error response с текстом statusText
"Not Found". Другие ошибки мы увидим позже в учебнике и обсудим их подробнее.
Пока же достаточно знать, что практически все ваши ошибки теперь будут обрабатываться этой страницей, а не бесконечными спиннерами, не реагирующими страницами или пустыми экранами 🙌.
Пользовательский интерфейс контактного маршрута¶
Вместо страницы 404 "Not Found" мы хотим действительно отображать что-то на URL, на которые мы ссылались. Для этого нам нужно создать новый маршрут.
👉 Создаем модуль маршрута контакта
1 |
|
👉 Добавить UI компонента контактов.
Это просто набор элементов, не стесняйтесь копировать/вставлять.
src/routes/contact.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
|
Теперь, когда у нас есть компонент, давайте подключим его к новому маршруту.
👉 Импортируем компонент contact и создаем новый маршрут
src/main.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Теперь, если мы щелкнем на одной из ссылок или посетим /contacts/1
, то получим наш новый компонент!
Однако он находится не внутри нашего корневого макета 😠.
Вложенные маршруты¶
Мы хотим, чтобы компонент контактов отображался внутри макета <Root>
следующим образом.
Для этого нужно сделать маршрут контактов дочерним по отношению к корневому маршруту.
👉 Переместить маршрут контактов в дочернее состояние корневого маршрута
src/main.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Теперь вы снова видите корневой макет, но пустую страницу справа. Нам нужно указать корневому маршруту, где он должен отображать свои дочерние маршруты. Мы сделаем это с помощью <Outlet>
.
Найдем <div id="detail">
и поместим аутлет внутрь
👉 Рендеринг <Outlet>
.
src/routes/root.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Маршрутизация на стороне клиента¶
Возможно, вы заметили, а возможно, и нет, но когда мы нажимаем на ссылки в боковой панели, браузер выполняет полный запрос документа для получения следующего URL, а не использует React Router.
Маршрутизация на стороне клиента позволяет нашему приложению обновлять URL без запроса другого документа с сервера. Вместо этого приложение может сразу отрисовать новый пользовательский интерфейс. Давайте сделаем это с помощью <Link>
.
👉 Изменим боковую панель <a href>
на <Link to>
src/routes/root.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Вы можете открыть вкладку network в браузере devtools и увидеть, что он больше не запрашивает документы.
Загрузка данных¶
Сегменты URL, макеты и данные чаще всего соединены (утроены?) вместе. Мы можем видеть это уже в этом приложении:
Сегмент URL | Компонент | Данные |
---|---|---|
/ | <Root> | список контактов |
contacts/:id | <Contact> | отдельный контакт |
Благодаря этой естественной связи React Router имеет соглашения о данных, позволяющие легко получать данные в компоненты маршрута.
Для загрузки данных мы будем использовать два API: loader
и useLoaderData
. Сначала мы создадим и экспортируем функцию загрузчика в корневом модуле, затем подключим ее к маршруту. Наконец, мы получим доступ к данным и отрисуем их.
👉 Экспорт загрузчика из root.jsx
src/routes/root.jsx | |
---|---|
1 2 3 4 5 6 7 |
|
👉 Настройка загрузчика на маршруте.
src/main.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
👉 Доступ к данным и их визуализация.
src/routes/root.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
|
Вот и все! Теперь React Router будет автоматически синхронизировать эти данные с вашим пользовательским интерфейсом. Пока у нас нет никаких данных, поэтому вы, вероятно, получите пустой список, как здесь:
Запись данных + HTML-формы¶
Через секунду мы создадим наш первый контакт, но сначала поговорим о HTML.
React Router эмулирует навигацию по HTML-формам как примитив мутации данных, согласно веб-разработке до камбрийского взрыва JavaScript. Это дает вам UX-возможности клиентских приложений с простотой "старой школы" веб-модели.
Хотя некоторые веб-разработчики не знают, что HTML-формы вызывают навигацию в браузере, как и щелчок по ссылке. Единственное различие заключается в запросе: ссылки могут изменять только URL, в то время как формы могут изменять метод запроса (GET или POST) и тело запроса (данные формы POST).
Без маршрутизации на стороне клиента браузер автоматически сериализует данные формы и отправляет их на сервер в виде тела запроса для POST и в виде URLSearchParams для GET. React Router делает то же самое, только вместо отправки запроса на сервер он использует маршрутизацию на стороне клиента и отправляет его по маршруту action
.
Мы можем проверить это, нажав кнопку "Создать" в нашем приложении. Приложение должно взорваться, поскольку сервер Vite не настроен на обработку POST-запроса (он посылает 404, хотя должен был бы посылать 405 🤷).
Вместо того чтобы отправлять POST на сервер Vite для создания нового контакта, давайте воспользуемся маршрутизацией на стороне клиента.
Создание контактов¶
Мы будем создавать новые контакты, экспортируя action
в наш корневой маршрут, подключая его к конфигурации маршрута и изменяя нашу <form>
на React Router <Form>
.
👉 Создайте действие и измените <form>
на <Form>
.
src/routes/root.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
👉 Импорт и установка действия на маршруте.
src/main.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Вот и все! Нажмите кнопку "Создать", и вы увидите, как в списке появится новая запись 🥳.
Метод createContact
просто создает пустой контакт без имени, данных или чего-либо еще. Но он все равно создает запись, обещаю!
🧐 Подождите секунду... Как обновляется боковая панель? Где мы вызвали action
? Где код для повторной выборки данных? Где useState
, onSubmit
и useEffect
?!
Именно здесь проявляется "старая школа веб-программирования". Как мы уже говорили, <Form>
не позволяет браузеру отправлять запрос на сервер и вместо этого посылает его в ваш маршрут action
. В веб-семантике POST обычно означает изменение некоторых данных. По условию, React Router использует это как подсказку для автоматической проверки данных на странице после завершения действия. Это означает, что все ваши хуки useLoaderData
обновляются, а пользовательский интерфейс автоматически синхронизируется с данными! Очень здорово.
URL-параметры в загрузчиках¶
👉 Нажмите на запись без имени.
Мы должны снова увидеть нашу старую статическую страницу контактов с одним отличием: теперь URL-адрес содержит реальный идентификатор записи.
Если просмотреть конфигурацию маршрута, то он выглядит следующим образом:
1 2 3 4 5 6 |
|
Обратите внимание на сегмент URL :contactId
. Двоеточие (:
) имеет особое значение, превращая его в "динамический сегмент". Динамические сегменты будут соответствовать динамическим (изменяющимся) значениям в данной позиции URL, например, идентификатору контакта. Мы называем эти значения в URL "URL Params", или просто "params" для краткости.
Эти params
передаются в загрузчик с ключами, соответствующими динамическому сегменту. Например, наш сегмент имеет имя :contactId
, поэтому его значение будет передано как params.contactId
.
Чаще всего эти параметры используются для поиска записи по идентификатору. Давайте попробуем это сделать.
👉 Добавляем загрузчик на страницу контактов и получаем доступ к данным с помощью useLoaderData
src/routes/contact.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
👉 Настройка загрузчика на маршруте.
src/main.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Обновление данных¶
Как и при создании данных, обновление данных производится с помощью <Form>
. Давайте создадим новый маршрут по адресу contacts/:contactId/edit
. Опять же, начнем с компонента, а затем подключим его к конфигурации маршрута.
👉 Создаем компонент edit
1 |
|
👉 Добавить пользовательский интерфейс страницы редактирования
Ничего такого, чего бы мы не видели раньше, не стесняйтесь копировать/вставлять:
src/routes/edit.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
|
👉 Добавить новый маршрут редактирования.
src/main.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Мы хотим, чтобы он отображался в аутлете корневого маршрута, поэтому мы сделали его дочерним по отношению к существующему дочернему маршруту.
(Вы можете заметить, что мы повторно использовали contactLoader
для этого маршрута. Это сделано только потому, что мы ленивы в этом учебнике. Нет смысла пытаться использовать общие загрузчики для маршрутов, они обычно имеют свои собственные.)
Итак, нажав на кнопку "Edit", мы получаем новый пользовательский интерфейс:
Обновление контактов с помощью FormData¶
Маршрут редактирования, который мы только что создали, уже отображает форму. Для обновления записи достаточно подключить к маршруту действие. Форма будет отправлена в действие, и данные будут автоматически перепроверены.
👉 Добавить действие в модуль редактирования
src/routes/edit.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
👉 Привязать действие к маршруту.
src/main.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Заполните форму, нажмите кнопку сохранить, и вы увидите нечто подобное! Только легче для глаз и, возможно, менее волосато.
Обсуждение мутации¶
😑 Это сработало, но я понятия не имею, что здесь происходит...
Давайте немного покопаемся...
Откройте файл src/routes/edit.jsx
и посмотрите на элементы формы. Обратите внимание, что у каждого из них есть имя:
src/routes/edit.jsx | |
---|---|
1 2 3 4 5 6 7 |
|
Без JavaScript при отправке формы браузер создаст FormData
и при отправке запроса на сервер установит его в качестве тела запроса. Как уже упоминалось, React Router предотвращает это и отправляет запрос вашему действию, включая FormData
.
Каждое поле в форме доступно с помощью formData.get(name)
. Например, для поля ввода, приведенного выше, можно получить доступ к имени и фамилии следующим образом:
1 2 3 4 5 6 |
|
Поскольку у нас несколько полей формы, мы использовали Object.fromEntries
, чтобы собрать их все в объект, что как раз и нужно нашей функции updateContact
.
1 2 3 |
|
Кроме action
, ни один из рассматриваемых нами API не предоставляется React Router: request
, request.formData
, Object.fromEntries
- все они предоставляются веб-платформой.
После того как мы завершили действие, обратите внимание на redirect
в конце:
src/routes/edit.jsx | |
---|---|
1 2 3 4 5 6 |
|
И загрузчики, и действия могут возвращать ответы
(это логично, ведь они получили запрос
!). Помощник redirect
просто упрощает возврат response, который сообщает приложению о необходимости сменить местоположение.
Без маршрутизации на стороне клиента, если сервер перенаправлялся после POST-запроса, новая страница получала последние данные и отрисовывалась. Как мы уже узнали, React Router эмулирует эту модель и автоматически перепроверяет данные на странице после выполнения действия. Именно поэтому боковая панель автоматически обновляется при сохранении формы. Лишний код повторной проверки не существует без маршрутизации на стороне клиента, поэтому он не нужен и при маршрутизации на стороне клиента!
Перенаправление новых записей на страницу редактирования¶
Теперь, когда мы знаем, как перенаправлять, давайте обновим действие, создающее новые контакты, чтобы оно перенаправляло на страницу редактирования:
👉 Перенаправление на страницу редактирования новой записи.
src/routes/root.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Теперь, когда мы нажимаем кнопку "New", мы должны попасть на страницу редактирования:
👉 Добавить горсть записей.
Я собираюсь использовать звездный состав докладчиков первой конференции Remix Conference 😁.
Стилизация активных ссылок¶
Теперь, когда у нас есть куча записей, неясно, на какую из них мы смотрим в боковой панели. Чтобы исправить это, мы можем использовать NavLink
.
👉 Использование NavLink
в боковой панели
src/routes/root.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
Обратите внимание, что мы передаем функцию в className
. Когда пользователь находится на URL в NavLink
, то isActive
будет true
. Когда он только собирается стать активным (данные еще загружаются), isPending
будет истиной. Это позволяет нам легко указать, где находится пользователь, а также обеспечить немедленную обратную связь со ссылками, которые были нажаты, но еще ожидают загрузки данных.
Глобальный отложенный пользовательский интерфейс¶
По мере того как пользователь перемещается по приложению, React Router будет оставлять старую страницу поднятой по мере загрузки данных для следующей страницы. Вы, возможно, заметили, что приложение немного не реагирует на нажатия кнопок в списке. Давайте обеспечим пользователю обратную связь, чтобы приложение не казалось невосприимчивым.
React Router управляет всем состоянием за кулисами и раскрывает его фрагменты, необходимые для создания динамических веб-приложений. В данном случае мы будем использовать хук useNavigation
.
👉 useNavigation
для добавления глобального отложенного пользовательского интерфейса
src/routes/root.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
useNavigation
возвращает текущее состояние навигации: оно может быть одним из "idle" | "submitting" | "loading"
.
В нашем случае мы добавляем класс "loading"
в основную часть приложения, если оно не простаивает. Затем CSS добавляет красивое затухание после небольшой задержки (чтобы избежать мерцания пользовательского интерфейса при быстрой загрузке). Однако вы можете сделать все, что угодно, например, показать спиннер или полосу загрузки сверху.
Обратите внимание, что наша модель данных (src/contacts.js
) имеет клиентский кэш, поэтому переход к одному и тому же контакту происходит быстро и во второй раз. Такое поведение - это не React Router, он будет заново загружать данные при изменении маршрута независимо от того, были ли вы там раньше или нет. Однако это позволяет избежать вызова загрузчиков для изменяющихся маршрутов (например, списка) во время навигации.
Удаление записей¶
Если просмотреть код маршрута контактов, то можно обнаружить, что кнопка удаления выглядит следующим образом:
src/routes/contact.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Обратите внимание, что action
указывает на "destroy"
. Как и <Link to>
, <Form action>
может принимать относительное значение. Поскольку форма отображается в contact/:contactId
, то относительное действие с destroy
при щелчке отправит форму в contact/:contactId/destroy
.
На этом этапе вы должны знать все, что нужно для работы кнопки удаления. Может быть, стоит попробовать, прежде чем двигаться дальше? Вам потребуется:
- Новый маршрут
- Действие на этом маршруте
deleteContact
изsrc/contacts.js
.
👉 Создание модуля маршрута "уничтожить "
1 |
|
👉 Добавить действие уничтожения
src/routes/destroy.jsx | |
---|---|
1 2 3 4 5 6 7 |
|
👉 Добавить маршрут уничтожения в конфигурацию маршрутов.
src/main.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Итак, перейдите к записи и нажмите кнопку "Удалить". Работает!
😅 Я все еще не понимаю, почему это все работает.
Когда пользователь нажимает на кнопку отправки:
-
<Form>
предотвращает стандартное поведение браузера, посылающего новый POST-запрос на сервер, а вместо этого эмулирует браузер, создавая POST-запрос с маршрутизацией на стороне клиента -
Команда
<Form action="destroy">
сопоставляет новый маршрут по адресу"contacts/:contactId/destroy"
и отправляет ему запрос. -
После перенаправления действия React Router вызывает все загрузчики данных на странице, чтобы получить последние значения (это и есть "ревалидация"). Функция
useLoaderData
возвращает новые значения и заставляет компоненты обновляться!
Добавьте форму, добавьте действие, React Router сделает все остальное.
Контекстные ошибки¶
Просто для интереса, бросьте ошибку в действие destroy:
src/routes/destroy.jsx | |
---|---|
1 2 3 4 5 |
|
Узнаете этот экран? Это наш предыдущий errorElement
. Однако пользователь не может ничего сделать, чтобы вернуться к этому экрану, кроме как нажать кнопку refresh.
Давайте создадим контекстное сообщение об ошибке для маршрута уничтожения:
src/main.jsx | |
---|---|
1 2 3 4 5 6 7 8 |
|
Теперь попробуйте еще раз:
Теперь у нашего пользователя есть больше возможностей, чем нажатие кнопки refresh, он может продолжать взаимодействовать с теми частями страницы, которые не вызывают проблем 🙌.
Поскольку маршрут уничтожения имеет собственный errorElement
и является дочерним по отношению к корневому маршруту, ошибка будет отображаться там, а не в корне. Как вы, вероятно, заметили, эти ошибки всплывают до ближайшего errorElement
. Добавляйте их сколько угодно и как угодно мало, лишь бы они были в корне.
Индексные маршруты¶
Когда мы загрузим приложение, вы заметите большую пустую страницу в правой части нашего списка.
Когда у маршрута есть дочерние маршруты, и вы находитесь на пути родительского маршрута, то <Outlet>
нечего отображать, поскольку нет ни одного дочернего маршрута. Индексные маршруты можно рассматривать как дочерние маршруты по умолчанию, заполняющие это пространство.
👉 Создание модуля индексного маршрута
1 |
|
👉 Заполнить элементы компонента index.
Не стесняйтесь копировать-вставлять, ничего особенного здесь нет.
src/routes/index.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
👉 Настройка индексного маршрута.
src/main.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Обратите внимание на { index:true }
вместо { path: "" }
. Это указывает маршрутизатору на то, что этот маршрут будет соответствовать и отображаться, когда пользователь находится на точном пути родительского маршрута, поэтому в <Outlet>
нет других дочерних маршрутов для отображения.
Вуаля! Больше нет пустого места. На индексных маршрутах принято размещать панели управления, статистику, ленты и т.д. Они также могут участвовать в загрузке данных.
Кнопка отмены¶
На странице редактирования у нас есть кнопка отмены, которая пока ничего не делает. Мы хотим, чтобы она выполняла те же действия, что и кнопка "Назад" в браузере.
Для этого нам понадобится обработчик нажатия на кнопку, а также useNavigate
из React Router.
👉 Добавьте обработчик нажатия на кнопку отмены с помощью useNavigate
src/routes/edit.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Теперь при нажатии кнопки "Отмена" пользователь будет возвращаться на одну запись в истории браузера.
🧐 Почему на кнопке нет event.preventDefault
?
Кнопка <button type="button">
, хотя и кажется излишней, является HTML-средством, позволяющим предотвратить отправку формы кнопкой.
Осталось еще две функции. Мы выходим на финишную прямую!
Параметры поиска URL и GET-запросы¶
Все наши интерактивные пользовательские интерфейсы до сих пор были либо ссылками, изменяющими URL, либо формами, отправляющими данные в действия. Поле поиска интересно тем, что представляет собой смесь того и другого: это форма, но она изменяет только URL, не изменяя данных.
Сейчас это обычная HTML <form>
, а не React Router <Form>
. Давайте посмотрим, что браузер делает с ней по умолчанию:
👉 Введите имя в поле поиска и нажмите клавишу Enter.
Обратите внимание, что URL браузера теперь содержит ваш запрос в виде URLSearchParams:
1 |
|
Если мы рассмотрим форму поиска, то она выглядит следующим образом:
src/routes/root.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 |
|
Как мы уже видели, браузеры могут сериализовать формы по атрибуту name
элементов ввода. Имя этого элемента ввода - q
, поэтому URL имеет вид ?q=
. Если бы мы назвали его search
, то URL был бы ?search=
.
Обратите внимание, что эта форма отличается от других, которые мы использовали, тем, что в ней нет <form method="post">
. По умолчанию используется метод
"get"
. Это означает, что когда браузер создает запрос на следующий документ, он помещает данные формы не в тело POST-запроса, а в URLSearchParams
GET-запроса.
GET-запросы с маршрутизацией на стороне клиента¶
Давайте используем маршрутизацию на стороне клиента для отправки формы и фильтрации списка в существующем загрузчике.
👉 Измените <form>
на <Form>
.
src/routes/root.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 |
|
👉 Фильтровать список при наличии URLSearchParams.
src/routes/root.jsx | |
---|---|
1 2 3 4 5 6 |
|
Поскольку это GET, а не POST, React Router не вызывает action
. Отправка GET-формы - это то же самое, что и щелчок по ссылке: меняется только URL. Поэтому код, который мы добавили для фильтрации, находится в loader
, а не в action
этого маршрута.
Это также означает, что это обычная постраничная навигация. Вы можете нажать кнопку "Назад", чтобы вернуться к тому, на чем остановились.
Синхронизация URL-адресов с состоянием формы¶
Здесь есть несколько UX-проблем, которые мы можем быстро решить.
-
Если после поиска щелкнуть на кнопке "Назад", то в поле формы останется введенное значение, хотя список больше не фильтруется.
-
Если обновить страницу после поиска, то в поле формы больше не будет введенного значения, хотя список отфильтрован.
Другими словами, URL и состояние нашей формы рассинхронизированы.
👉 Возвратите q
из вашего загрузчика и установите его в качестве значения по умолчанию для поля поиска
src/routes/root.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
Это решает проблему (2). Если теперь обновить страницу, то в поле ввода появится запрос.
Теперь о проблеме (1) - нажатии кнопки "Назад" и обновлении ввода. Мы можем привнести useEffect
из React, чтобы напрямую манипулировать состоянием формы в DOM.
👉 Синхронизация значения ввода с параметрами поиска URL
src/routes/root.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
🤔 Не лучше ли использовать для этого управляемый компонент и React State?
Конечно, можно сделать это в виде управляемого компонента, но в итоге получится больше сложностей для того же поведения. Вы не управляете URL, это делает пользователь с помощью кнопок назад/вперед. В управляемом компоненте будет больше точек синхронизации.
Если вас все еще беспокоит этот вопрос, разверните его, чтобы увидеть, как это будет выглядеть
Обратите внимание, что для управления входом теперь требуется три точки синхронизации, а не одна. Поведение идентично, но код стал сложнее.
src/routes/root.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
|
Отправка форм onChange
¶
Здесь нам предстоит принять решение по продукту. Для данного пользовательского интерфейса мы, вероятно, предпочли бы, чтобы фильтрация происходила при каждом нажатии клавиши, а не при явном отправлении формы.
Мы уже видели useNavigate
, для этого воспользуемся его родственником, useSubmit
.
src/routes/root.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
Теперь при вводе текста форма отправляется автоматически!
Обратите внимание на аргумент submit
. Мы передаем event.currentTarget.form
. currentTarget
- это узел DOM, к которому привязано событие, а currentTarget.form
- родительский узел формы ввода. Функция submit
сериализует и отправляет любую переданную ей форму.
Добавление поискового спиннера¶
В производственном приложении поиск, скорее всего, будет искать записи в базе данных, которая слишком велика, чтобы отправлять их все сразу и фильтровать на стороне клиента. Именно поэтому в демонстрационном варианте присутствует некоторая имитация сетевой задержки.
Без какого-либо индикатора загрузки поиск выглядит довольно вялым. Даже если бы мы могли сделать нашу базу данных быстрее, нам всегда будет мешать сетевая задержка пользователя, которую мы не можем контролировать. Чтобы улучшить UX, давайте добавим немедленную обратную связь для поиска. Для этого мы снова используем useNavigation
.
👉 Добавляем крутилку поиска
src/routes/root.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
|
Значение navigation.location
появляется, когда приложение переходит на новый URL и загружает данные для него. Затем он исчезает, когда навигация больше не выполняется.
Управление стеком истории¶
Теперь, когда форма отправляется на каждое нажатие клавиши, если мы напечатаем символ "seba", а затем удалим его с помощью backspace, то в стеке появится 7 новых записей 😂. Нам это точно не нужно
Мы можем избежать этого, если будем замещать текущую запись в стеке истории на следующую страницу, а не проталкивать в нее.
👉 Использование replace
в submit
src/routes/root.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
Мы хотим заменить только результаты поиска, а не страницу до начала поиска, поэтому мы делаем быструю проверку, является ли это первым поиском или нет, и затем принимаем решение о замене.
Каждое нажатие клавиши больше не создает новых записей, поэтому пользователь может вернуться из результатов поиска без необходимости нажимать на нее 7 раз 😅.
Мутации без навигации¶
До сих пор во всех наших мутациях (изменениях данных) использовались формы с навигацией, создающие новые записи в стеке истории. Хотя такие пользовательские потоки встречаются довольно часто, не менее часто возникает желание изменить данные без навигации.
Для таких случаев у нас есть хук useFetcher
. Он позволяет нам взаимодействовать с загрузчиками и экшенами, не вызывая навигации.
Кнопка ★ на странице контактов имеет для этого смысл. Мы не создаем и не удаляем новую запись, мы не хотим менять страницы, мы просто хотим изменить данные на странице, которую мы просматриваем.
👉 Измените форму <Favorite>
на форму поиска
src/routes/contact.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Возможно, стоит взглянуть на эту форму, пока мы здесь. Как обычно, наша форма содержит поля с параметром name
. Эта форма отправит formData
с ключом favorite
, который является либо "true" | "false"
. Поскольку у нее есть method="post"
, она вызовет действие. Поскольку нет реквизита <fetcher.Form action="...">
, он будет отправлен на маршрут, где отображается форма.
👉 Создание действия
src/routes/contact.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Довольно просто. Извлеките данные формы из запроса и отправьте их в модель данных.
👉 Конфигурируем новое действие маршрута
src/main.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Итак, мы готовы нажать на звездочку рядом с именем пользователя!
Проверьте, обе звезды автоматически обновляются. Наша новая <fetcher.Form method="post">
работает почти так же, как и <Form>
, которую мы использовали раньше: она вызывает действие, а затем все данные автоматически перепроверяются - даже ошибки будут отлавливаться точно так же.
Однако есть одно ключевое отличие: это не навигация - URL не меняется, стек истории не затрагивается.
Оптимистичный пользовательский интерфейс¶
Вы, вероятно, заметили, что приложение не реагирует на нажатие кнопки "Избранное" из предыдущего раздела. Мы снова добавили некоторую сетевую задержку, потому что в реальном мире она будет иметь место!
Чтобы дать пользователю обратную связь, мы можем перевести звезду в состояние загрузки с помощью fetcher.state
(очень похоже на navigation.state
из предыдущего раздела), но в этот раз мы можем сделать кое-что еще лучше. Мы можем использовать стратегию, называемую "оптимистичным UI".
Фетчеру известны данные формы, отправляемые в действие, поэтому они доступны в fetcher.formData
. Мы будем использовать это для немедленного обновления состояния звезды, даже если сеть еще не завершила работу. Если обновление в итоге не произойдет, то пользовательский интерфейс вернется к реальным данным.
👉 Считываем оптимистичное значение из fetcher.formData
src/routes/contact.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Если теперь нажать на кнопку, то можно увидеть, как звезда мгновенно переходит в новое состояние. Вместо того чтобы постоянно отображать фактические данные, мы проверяем, не отправляются ли в fetcher какие-либо formData
, и если да, то используем их. Когда действие будет выполнено, fetcher.formData
перестанет существовать, и мы вернемся к использованию фактических данных. Таким образом, даже если в оптимистичном коде пользовательского интерфейса будут допущены ошибки, в конечном итоге он вернется к правильному состоянию 🥹.
Not Found Data¶
Что произойдет, если контакт, который мы пытаемся загрузить, не существует?
Наш корень errorElement
ловит эту неожиданную ошибку, когда мы пытаемся вывести контакт null
. Хорошо, что ошибка была правильно обработана, но мы можем добиться большего!
Когда в загрузчике или действии возникает ожидаемая ошибка, например, несуществующие данные, можно throw
. Стек вызовов сломается, React Router поймает его, и вместо него будет отрисован путь ошибки. Мы даже не будем пытаться отрисовать null
контакт.
👉 Выбросить ответ 404 в загрузчик
src/routes/contact.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
Вместо того чтобы выдать ошибку рендеринга Cannot read properties of null
, мы полностью избегаем этого компонента и вместо него выводим путь ошибки, сообщая пользователю что-то более конкретное.
Таким образом, ваши счастливые пути остаются счастливыми. Элементам маршрута не нужно беспокоиться об ошибках и состояниях загрузки.
Беспутевые маршруты¶
И последнее. Последняя страница с ошибкой, которую мы видели, была бы лучше, если бы она выводилась внутри корневого аутлета, а не на всю страницу. На самом деле, все ошибки во всех наших дочерних маршрутах лучше выводить в аутлете, тогда у пользователя будет больше возможностей, чем просто нажать кнопку refresh.
Мы хотели бы, чтобы это выглядело следующим образом:
Мы могли бы добавить элемент error в каждый из дочерних маршрутов, но, поскольку речь идет об одной и той же странице ошибки, делать это не рекомендуется.
Есть более чистый способ. Маршруты можно использовать без пути, что позволяет им участвовать в компоновке пользовательского интерфейса, не требуя новых сегментов пути в URL. Посмотрите:
👉 Вернуть дочерние маршруты в маршрут без пути
src/main.jsx | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
При возникновении ошибок в дочерних маршрутах наш новый беспутевой маршрут перехватит их и отрисует, сохранив пользовательский интерфейс корневого маршрута!
JSX-маршруты¶
И последний прием: многие предпочитают настраивать маршруты с помощью JSX. Это можно сделать с помощью функции createRoutesFromElements
. Функциональной разницы между JSX и объектами при конфигурировании маршрутов нет, это просто стилистическое предпочтение.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
Вот и все! Спасибо, что попробовали React Router. Мы надеемся, что это руководство поможет вам начать работу над созданием отличного пользовательского опыта. С помощью React Router можно сделать гораздо больше, поэтому обязательно ознакомьтесь со всеми API 😀.