С помощью этого туториала вы сможете лучше понять, как работает роутинг в реакт приложениях. Так же будет рассмотрена асинхронная загрузка данных.
За основу, будет взят React-router 4 и его компоненты Router и Link с минимальным кодом.
Нам предстоит сделать книжную полку
Этот материал был разобран на вебинаре, поэтому есть видео-версия происходящего.
Исходный код, само собой.
Для начала нужно развернуть приложение с помощью create-react-app
Если вы используете yarn
, то можете писать yarn start
Что такое роутинг в рамках SPA приложения?
Роутинг — это соответствие между контентом на экране и URL-адресом в адресной строке браузера.
Задача
Сделать приложение, в котором будет три ссылки:
- Главная — http://localhost:3000.
- Эпопеи — http://localhost:3000/epic — страница, на которой будет возможность выбрать серию произведений (Гарри Поттер, Властелин колец или Ганнибал).
- О приложении — http://localhost:3000/about — немного справочной информации о приложении в виде render-функции, а не полноценного компонента.
Следовательно, на языке реакта нам нужно сделать три роута (_Route_)
Спроектируем страницу
Прикинем как будет выглядеть компонент App:
src/components/App.js
Ничего особенного. Но есть недостающие компоненты Link
и Route
Создаем компонент Link
Компонент Link достаточно прост:
src/components/Router.js
В методе render
возвращается обычная ссылка, а вот click
по ссылке выглядит интереснее.
В функциях historyPush
/ historyReplace
используется стандартный объект браузера — window.history. Вновь никакой магии в мире JavaScript, увы но React-Router делает то же самое: изменяет историю с помощью нативных методов pushState и replaceState. Эти методы, «добавляют» (push) или «заменяют» (replace) записи в объекте history. Таким образом браузер изменяет URL-адрес без перезагрузки страницы.
Создаем компонент Route
В коде нашего приложения, роуты выглядят так:
Route — это обычный React-компонент, он принимает следующие свойства:
Первый роут (на самом деле, по английски произносится «рут») — рисует компонент Home
, только если URL-адрес равен http://localhost:3000/ (строго!)
Второй роут (/_epic_) рисует компонент Epic
, причем будет его рисовать, даже если адрес будет http://localhost:3000/epic/bla-bla/bla-bla-bla-bla, потому что не указан атрибут exact
.
Третий роут, примечателен тем, что он не содержит свойства component
, а содержит сразу функцию, которая должна отработать, если в URL-адресе, появилось /about
.
Супер! Требования для комопонета поставлены, приступим к его реализации.
src/components/Router.js
Если мы откроем наше приложение, то увидим следующую картину: все роуты будут отрисованы, при этом — клики по ссылкам будут изменять URL-адрес корректно, однако, наше приложение никак не будет реагировать на это. Зато, будет реагировать браузер! Если вы начнете кликать по ссылкам, то увидите, что кнопка «Назад» стала активной. Раз уж мы пользуемся «нативными» возможностями, то, разумеется, браузер сразу же начинает «понимать» нас.
Обучаем Route отображать только нужный компонент
Нам необходимо создать переменную-флаг, которая будет сигнализировать о том, нужно ли отрисовывать компонент. Здесь нет никаких затруднений, так как если мы из метода render
что-то возвращаем с помощью ключевого слова return
— то код ниже строки в которой указан return
не выполняется. Исключение — когда мы из return
возвращаем что-либо в круглых скобках, в таком случае, JavaScript выполнит все, что указано в скобках и покинет функцию.
У React-router есть свойство match
, в котором содержится полезная информация. Сделаем подобное.
Для начала создадим вспомогательную функцию.
src/components/Router.js
Теперь, воспользовавшись этой функцией, мы можем убить сразу двух зайцев:
- выбирать в
render
методеRoute
— что рисовать, а что нет. - передавать полезные свойства в качестве props в компонент, который будем рисовать роутом.
src/components/Router.js
Ок, сейчас у нас в приложении не отрисовываются сразу все компоненты. Но проблемы остались — клики по ссылкам, а так же кнопки назад/вперед — по прежнему не «перерисовывают» контент. Спасает только перезагрузка страницы, после которой отрисовывается то, что нужно.
[1] — фунция matchPath использует стандартное свойство window.location.pathname.
Обучаем Route реагировать на изменение URL-адреса
Как и у всего в мире JS, у window.history
есть своя событийная модель. Нас интересует событие popstate.
Поскольку, мы занимаемся разработкой «библиотеки», то нам и все карты в руки. Мы уже использовали React.createElement, пора задействовать и forceUpdate.
forceUpdate
— позволяет нам жестко указать реакту — перерисуй компонент (т.е. вызови метод render
).
src/components/Router.js
Попробуйте!
Клики по ссылкам по-прежнему только лишь изменяют URL-адрес, но кнопки «назад/вперед» — работаю корректно. Почему так? Потому что, событие popstate
срабатывает только при нажатии на эти кнопки. К моему большому сожалению, pushState
/ replaceState
не «генерируют» события popstate
. Я даже позволю себе смайлик ;(
Выход есть:
- регистрировать каждый Роут в неком массиве
- по клику на ссылку (
onClick
в компонентеLink
) идти по «зарегистрированным компонентам» и вызывать на каждомforceUpdate
.
Вуаля, теперь работает: при клике по ссылке, при переходах с помощью назад/вперед — контент меняется без перезагрузки страницы.
Чтобы красиво закончить наш «роутер», перепишем компонент Epic таким образом, чтобы он отображал ссылки на серии о Гарри Поттере, Властелине Колец и Ганнибале. Хорошая компания!
Из компонента App, нужно удалить определение компонента Epic и добавить import.
src/components/Epic.js
Обратите внимание, что мы используем «полезности» из свойства match, которое заблаговременно передаем.
Итого, path всех роутов будет /epic/название_серии
Следовательно, если мы захотим изменить epic на что-то другое, нам не нужно будет «бегать автозаменой» по всем файлам. Удобно.
Асинхронная загрузка данных
Роутер готов, можем приступать ко второй части — загрузка данных.
Какова задача? Нужно при клике на ссылку — рисовать компонент Book
и в нем запрашивать данные.
Чтобы компонент запрашивал данные, нужное где-то данные разместить. Для удобства будем использовать сервис — MockAPI, который позволяет имитировать рабочий сервер: то есть, по GET запросу, мы будем получать данные по книгам.
Сервис MockAPI настолько прост, что после регистрации, нужно лишь указать имя проекта и желаемые данные. Обратите внимание, что сервис поддерживает GET/PUT/DELETE с параметром id. Нам это пока что не пригодится.
Для нашего приложения, мы можем выбрать любую вложенность данных. Для простоты будем использовать несколько ресурсов: potter, lord, hannibal. Внутри которых будут похожие данные.
Пример данных для ресурса potter:
Обычный JSON: имя (_name_), URL адрес для картинки (_imgUrl_), описание (_description_).
Для получения данных асинхронно, будем использовать нативный fetch. А так же ознакомимся с async/await.
Процесс работы с асинхронными данными не отличается от обычной работы с данными, которые изменяются. Уже уловили откуда дует ветер? Если данные изменяются, значит место им в state.
Прикинем, что нам нужно хранить в state компонента, который запрашивает данные (по другому, такой компонент еще называют «контейнером«):
- флаг — данные загружаются или не загружаются (обычно это
isLoading: true/false
) - сами данные — начальное состояние данных — «пусто», после того как запрос отработает — «густо». К примеру, в нашем примере, нам необходимо будет из ответа с MockAPI брать массив
books
и его «класть» в state. Значит, начальное состояние данных в state — пустой массив.
src/components/Book.js
Листинг кода содержит подробные комментарии. Думаю для тех, кто знаком с работой функций map и promise, такой код не покажется сложным.
Так же, в коде я использую самописную функцию-хелпер (_помощницу_) httpGet
, так как предполагается, что в приложении много где будет нужно запрашивать данные с сервера, и чтобы не дублировать код, он вынесен в отдельную функцию.
src/helpers/network.js
Обратите внимание, что у нас отсутствует обработка ошибок, и если запрос завершится неудачно — все приложение «умрет». Мы исправим этот момент в заключительном разделе.
А пока создадим компонент BookInfo
, чтобы посмотреть на результат в действии.
src/components/BookInfo.js
Самый обычный компонент, разве что, вместо div
я использовал возможности Реакта 16й версии — React.Fragment.
Обработка ошибок
Сейчас при запросе книг про Ганнибала Лектера, MockAPI возвращает 404, так как я не указал никаких JSON-данных для этого ресурса.
Во-первых, в рамках JS мы должны обрабатывать такую ошибку с помощью блока try..catch.
src/helpers/network.js
Во-вторых, мы должны обновить компонент Book
:
- добавить в начальное состояние
error: false
- изменять
error
наtrue
если произошла ошибка, иisLoading
наfalse
, так как данные уже не загружаются - отображать предложение «проиозшла ошибка + иконку перезагрузки» для пользователя
- при выполнении нового запроса (по нажатию на иконку перезагрузки) — запускать все по новый, то есть: обновить флаг
isLoading
наtrue
, стереть ошибку (error: false
) и выполнить запрос.
src/components/Book.js
Результат:
Готово! Теперь можно взять кружку чая и погрузиться в одну из этих замечательных серий. Спасибо за внимание.
Добрый день. Зачем создавать компоненты Router и Link если она уже есть в react-router-dom?
Добрый день, это была тема вебинара, на котором мы пытались создать «свой» роутер. То есть, изучали как это может быть «изнутри».
жесть, автор постарался)