001 Что такое API и как работают реальные приложения
API (Application Programming Interface) - это набор готовых функций и свойств, которые можно использовать. Либо это можно назвать договором - мы обязуемся использовать правильный синтаксис и использовать допустимые запросы для того, чтобы получить нужные данные или что-то выполнить, а АПИ обязуется выполнить то, что мы ему отправили.
Вот пример открытого API, который готов нам выдать информацию с сервера, если мы только правильно введём fetch-запрос с нужной ссылкой - контракт - я правильно запрашиваю, а ты выдаёшь
Конкретно в мире веб-разработки API говорит нам, как мы будем реализовывать общение фронт-енд части приложения с сервером
Так же есть отдельная страница на гитхабе, где есть огромный список всех публичных API
002 Новый проект и работа с сервером
Будет разрабатываться реальный сайт на основе данных, полученных с сайта по вселенной Marvel. Отсюда нам нужно только произвести настройку доступности отправляемых запросов (ставим ото всюду *.) и сам ключ для совершения запросов abfdaba95091affea928543eb9253ded
Чтобы не писать путь до изображения, можно просто вставить его как объект - вебпак поймёт
Далее нужно реализовать сервис, который будет отвечать за взаимодействие фронта с бэком
И тут мы описываем класс, который будет содержать методы, получающие данные с сервера
src > services > marvel.service.js
Получим для примера ответ от сервера в виде объекта:
src > index.js
Первым нам вышел персонаж по id, а вторым ответом мы получили ограниченный массив персонажей (9 штук)
Далее нам нужно доработать метод - вынесем повторяющиеся данные в отдельные поля класса
src > services > marvel.service.js
И далее попробуем вывести массив имён из того ответа, который приходит нам с сервера:
src > index.js
003 Трансформация данных и компонент со случайным персонажем
Самое первое, что нужно реализовать - это получение данных по персонажу с сервера:
Метод _transformCharacter() принимает в себя данные с сервера и возвращает данные отформатированные под тот формат, что нужно использовать в приложении. Такой подход заменит работу с интерфейсом.
Методы getAllCharacters() и getCharacter() модифицируем таким образом, чтобы они как и раньше принимали и сохраняли в себе результат запроса и возвращали во внешние модули только отформатированный запрос через _transformCharacter()
src > services > marvel.service.js
Далее переходим к отображению данных на фронте:
создаём классовый компонент
создадим его состояние, которое будет хранить в себе объект персонажа
далее инициализируем сервис по работе с сервером
далее создадим функцию onCharLoaded, которая будет менять состояние персонажа компонента
в функции updateChar мы генерируем id персонажа и дёргаем сервер, получая информацию о персонаже (уже заранее отформатированную сервисом) и далее устанавливаем состояние в компоненте
метод sliceDescription обрежет строку, так как описание полученное с сервера, может выходить за пределы карточки
src > components > randomChar > RandomChar.js
Так же хочется отметить про несколько видов записей функций: если мы просто вставим в функцию вызов другой функции, то вложенная функция вызовется с переданным дефолтным аргументом родительской функции (первой)
И тут нужно сразу сказать, что такой подход, который был описан выше - неправильный. Работать со стейтом, подписываться на события и создавать сервисы при конструировании компонента является плохой практикой
Итог: мы имеем при перезагрузке страницы рандомного персонажа
Оформление работы с сервером:
Если наше приложение взаимодействует с сервером, то сетевую логику нужно отделять от реализации фронтенда
В остальных компонентах нужно использовать только результаты работы данного сервиса
004 Хороший тон приложения (спиннер, ошибки…)
Далее на сайт нужно добавить спиннер, который будет показывать пользователю, что информация загружается - это его успокоит и упростит взаимодействие с сайтом
Первым делом создаём компонент со спиннером, который будем использовать на странице
src > components > Spinner > Spinner.js
Так будет выглядеть компонент ошибки:
src > components > ErrorMessage > ErrorMessage.js
Так же можно отметить, что мы можем положить нужное изображение в паблик папку и получить нужное нам изображение через переменную окружения
И тут мы реализуем два новых состояния и метод onError, которые будут отвечать за отображение ошибки и спиннера
src > components > randomChar > RandomChar.js
Итог: мы имеем отображение загрузки и ошибки
005 Жизненный цикл компонентов
Сейчас воспроизведём одну проблему в работе компонентов:
переведём компонент App в классовый и внутри него создадим возможность удалять компонент со страницы, показывая null вместо него
Далее в самом компоненте будем внутри конструктора по интервалу вызвать запрос на получение данных updateChar
И несколько раз скроем и покажем компонент
В итоге мы получим ситуацию, когда запрос на получение данных со страницы уходит на просто за раз по два раза (что уже плохо), но так же и компоненты, которые мы насоздавали - не исчезают и продолжают отправлять запросы на сервер, что приводит к отправке более чем двух запросов в секунду. Такой подход создаёт сильную угрозу утечки данных
И тут нам нужно перейти к жизненному циклу компонента, чтобы понять, каким образом у нас происходят данные ошибки
Жизненный цикл компонента делится на 3 этапа (если не включать состояние ошибки) и предполагает под собой 3 хука этих состояний:
componentDidMount - компонент появляется на странице
componentDidUpdate - компонент обновляется
componentWillUnmount - компонент уходит со страницы
И данную структуру нужно знать:
Первым этапом у нас идёт монтирование
вызывается конструктор
после конструктора идёт render компонента
потом обновляется DOM-дерево
и в конце вызывается componentDidMount
Вторым этапом у нас идёт обновление компонента
обновление компонента вызвают:
изменение пропсов
установление нового состояния через setState()
насильное обновление через forceUpdate()
И мы опять попадаем в метод render, который монтирует наш компонент
Дальше обновляется дерево
И срабатывает хук componentDidUpdate
Третьим этапом мы просто размонтируем компонент и стираем со страницы
тут вызывается хук componentWillUnmount
Расставим на всех контрольных точках приложения логи определённого этапа монтирования компонента
Сейчас закомментируем вызов функции обновления персонажа на странице, чтобы у нас нормально работал компонент.
И тут можно увидеть, что компонент рендерится ровно один раз
Но если мы вернём в конструктор данную запись, то можно увидеть, что компонент обновился два раза, что и привело к двойному срабатыванию функции отправки запроса на сервер.
Это происходит потому, что мы обновляем состояние и запрашиваем рендер компонента до того, как он отрендерился в первый раз. Из-за такого наслоения вместо одного рендера происходит перерендер с новым состоянием.
Нам можно использовать обновления состояний компонента только на этапе "commit"
И чтобы поправить ошибку, просто вызовем функцию отправки запроса на сервер на этапе коммита компонента, а именно в методе componentDidMount()
Ну и чтобы при размонтировании компонента у нас остановился и таймер, нужно останавливать этот таймер на этапе размонтирования компонента
Так же нужно отметить:
Что данную отписку нужно выполнять всегда, так как она продолжит работать, даже если мы перейдём на другую страницу (все компоненты уничтожатся, а подписка останется)
Что если мы добавили подписку через стандартное DOM-api (например, addEventListener), то и удалять эту подписку нужно через DOM-api (тут - removeEventListener)
006 Практика с жизненным циклом, componentDidUpdate
Сейчас нам нужно реализовать загрузку списка персонажей на странице, а так же реализовать вывод информации по ним при нажатии на карточку в боковом меню страницы
В компоненте App добавим состояние selectedChar, которое будет хранить id выбранного персонажа и метод onSelectedChar, который мы будем вызывать внутри компонента CharList, чтобы получить этот нужный нам id персонажа
src > components > app > App.js
Таким образом реализована логика списка персонажей на странице
Так же при клике на карточку будет вызваться функция onCharSelected(), которая вернёт в компонент App нужный нам id персонажа
src > components > charList > CharList.js
Так же добавим получение комиксов с сервера
src > service > marvel.service.js
И далее нам нужно реализовать рендер компонента информации персонажей:
при загрузке страницы componentDidMount обновляем персонажа updateChar
при обновлении пропсов так же через componentDidUpdate обновляем персонажа
src > components > charInfo > CharInfo.js
Итог: до нажатия на персонажей - отображается скелетон, а уже при нажатии на персонажа, у нас подгружаются данные в info для отображения
007 Предохранители (Error Boundaries)
Попробуем воспользоваться методом, который срабатывает при появлении ошибки в компоненте componentDidCatch. В нём мы установим состояние ошибки компонента и выведем информацию о полученных аргументах.
src > component > charInfo > CharInfo.js
И, как мы видим, ошибка всё равно выскакивает и в консоли нет информации из метода. Логика данного хука была изменена в 16 версии реакта и теперь она не предотвращает краш всей страницы. Это было сделано с целью предотвращения отправки на сервер некорректных данных.
И тут мы переходим к такому подходу, как Error Boundary (предохранители) - это функционал, который собой оборачивает наш компонент, и если в нём происходит ошибка, то он её отлавливает
Подобный компонент можно сделать только через класс - в функциональном подходе он ещё не реализован
И так выглядит реализация классового компонента ErrorBoundary:
создаём состояние ошибки
метод componentDidCatch будет совершать определённое действие при отлове ошибки (выведет нам объекты в консоль и запишет состояние ошибки)
далее по условию выведем компонент ошибки, если переловили ошибку, а если не переловили, то выведем просто обёрнутый компонент
Метод getDerivedStateFromError тут не нужен, но он может пригодиться, если нам нужно только записать состояние внутри данного класса
Когда мы меняем состояние через методы, можно обращаться по полному пути к состоянию
А можно деструктурировать нужное нам свойство
Далее в сервисе укажем отдельным полем число оффсета и вставим его в запрос. По умолчанию оффсет внутри метода будет принимать в себя значение поля
src > service > marverl.service.js
Далее нужно добавить функционал пагинации в наше приложение. Пагинация - это дозагрузка контента страницы по запросу пользователя.
изменим состояние charList на массив, добавим 3 состояния: newItemLoading (состояние загрузки новых персонажей), offset (отступ), charEnded (кончился ли список персонажей)
Добавим метод onRequest, который будет осуществлять запрос на сервер. Он будет принимать в себя тот оффсет, который нужно отступить
Добавим спиннер при загрузке списка перснажей onCharListLoading
Модифицируем метод onCharListLoaded так, чтобы он редактировал состояния при загрузке персонажей и проверял, кончился ли список
Далее в рендере добавляем на кнопку по условию атрибут disabled, которая заблокирует кнопку, если список грузится, добавим стили под блокировку кнопки и добавим функцию onRequest, которая дозагрузит персонажей
src > components > charList > CharList.js
Ну и так же накинем на кнопку фильтр, чтобы пользователь понимал, что она некликабельна через :disabled
styles > button.scss
Итог: у нас подгружаются новые персонажи и кнопка становится тёмной
И так выглядит самая простая реализация типизации пропсов:
И если у нас будет свойство charId: PropTypes.string, то мы получим подобную ошибку
Проверка типов с помощью PropTypes
Примечание:
С версии React 15.5 React.PropTypes были вынесены в отдельный пакет. Так что используйте библиотеку prop-types.
Вы можете использовать codemod-скрипт, чтобы провести замену в коде на использование этой библиотеки.
По мере роста вашего приложения вы можете отловить много ошибок с помощью проверки типов. Для этого можно использовать расширения JavaScript вроде Flow и TypeScript. Но, даже если вы ими не пользуетесь, React предоставляет встроенные возможности для проверки типов. Для запуска этой проверки на пропсах компонента вам нужно использовать специальное свойство propTypes:
В данном примере проверка типа показана на классовом компоненте, но она же может быть применена и к функциональным компонентам, или к компонентам, созданным с помощью React.memo или React.forwardRef.
PropTypes предоставляет ряд валидаторов, которые могут использоваться для проверки, что получаемые данные корректны. В примере мы использовали PropTypes.string. Когда какой-то проп имеет некорректное значение, в консоли будет выведено предупреждение. По соображениям производительности propTypes проверяются только в режиме разработки.
PropTypes
Пример использования возможных валидаторов:
Требование одного дочернего элемента
С помощью PropTypes.element вы можете указать, что только один дочерний элемент может быть передан компоненту в качестве потомка.
Значения пропсов по умолчанию
Вы можете задать значения по умолчанию для ваших props с помощью специального свойства defaultProps:
C ES2022 вы можете объявить defaultProps как статическое свойство внутри классового React компонента. Подробнее можно узнать в статье про публичные статические поля класса. Для поддержки этого современного синтаксиса в старых браузерах потребуется компиляция.
Определение defaultProps гарантирует, что this.props.name будет иметь значение, даже если оно не было указано родительским компонентом. Сначала применяются значения по умолчанию, заданные в defaultProps. После запускается проверка типов с помощью propTypes. Так что проверка типов распространяется и на значения по умолчанию.
Функциональные компоненты
К функциональным компонентам можно также применять PropTypes.
Допустим, есть такой компонент:
Для добавления PropTypes нужно объявить компонент в отдельной функции, которую затем экспортировать:
А затем добавить PropTypes напрямую к компоненту HelloWorldComponent:
010 Вставка элементов через props.children
Пропс children в React содержит в себе все те элементы, что мы передали внутрь компонента между тегами <тег>ЭТО_CHILDREN</тег>
Создадим компонент DynamicGreating, который будет выводить элементы в определённой карточке. Внутри него мы будем выводить дочерние элементы через props.children.
Далее вызовем этот компонент в App и обернём внутрь него определённые данные
Так же, если мы передаём несколько элементов в качестве ребёнка, то они образуют собой массив, для которого работают стандартные методы высшего порядка
map вызывается для каждого непосредственного потомка, содержащегося в children передавая их по очереди в thisArg. Если children — это массив, он будет пройден, и функция будет вызвана для каждого потомка в массиве. Если children равен null или undefined, этот метод вернёт null или undefined, а не массив.
Метод cloneElement клонирует и возвращает новый React-элемент, используя элемент в качестве отправной точки. config должен содержать все новые пропсы, key, а также ref Полученный элемент будет иметь пропсы исходного элемента, а новые пропсы будут поверхностно слиты воедино. Новые дочерние элементы заменят существующие. key и ref из исходного элемента будут сохранены, если в config не было передано key и ref.
Первый подход
Тут будет пройден массив из всех переданных элементов внутрь компонента DynamicGreating и на каждого ребёнка будут повешены классы стилей
Второй подход
Мы так же можем заранее вписать в определённую заготовку нужные нам пропсы, которые она будет принимать
И далее просто пропсами передаём вёрстку
И на выходе мы получим данную переданную вёрстку, отображённую внутри заготовки
011 Специализация и наследование
Наследование в React у нас обычно происходит от Component для классов
Композиция же предполагает под собой специализирование компонента за счёт использования другого компонента.
Тут мы из компонента динамического приветствия реализовали специфическое HelloGreating
Что лучше использовать: композицию или наследование?
В React реализованы все инструменты для удобного использования композиции (когда мы специфицируем один компонент за счёт использования его внутри другого).
Сами разработчики React не находили надобности в развитии наследования компонентов
012 Render-props паттерн
Обычно, чтобы передавать состояние из одного компонента в другой, мы пользуемся таким подходом:
Но иногда нам может понадобиться логика определённого компонента сразу в нескольких местах, что заставит нас переделывать один и тот же компонент
Чтобы можно было воспользоваться логикой одного компонента внутри другого компонента и не делать жёсткую вёрстку, можно воспользоваться следующим подходом:
Такой подход называется Render-props, когда мы передаём внутрь одного компонента в качестве пропса другой компонент на отрисовку
При таком подходе мы передаём в компонент функцию, которая при вызове возвращает вёрстку и на вход принимает аргументы от родителя
013 Что такое ref и зачем он нужен
Стартовый проект:
ref - это ссылка на элемент вёрстки из ДОМ-дерева на странице
Чтобы создать ссылку в классовом компоненте, нужно вызвать через React функцию createRef(). Далее на нужный нам элемент навешиваем данный реф.
Так же можно рефы указывать в полях класса
И далее мы можем воспользоваться стандартными функциями API браузера на элементах вёрстки
И сейчас можно увидеть, что фокус срабатывает на форме
Если нам нужно будет навесить ref на компонент React, то тут уже придётся проделать определённые манипуляции
И теперь, чтобы мы смогли воспользоваться рефом компонента у нас есть два пути:
обернуть функциональный компонент в forwardRef и прокинуть вторым аргументом ref в нужное нам поле компонента
можно просто сделать компонент классовым
Но проблема теперь будет заключаться в том, что мы не сможем воспользоваться стандартным API и вызвать focus(), так как он вызывается сейчас на компоненте, а не на элементе ДОМ-дерева. Однако мы можем вызвать функции самого компонента, поэтому можно будет вызывать фокус в самом компоненте.
Так же мы имеем такой подход как коллбэк-реф. Это подход, при котором мы должны навешивать реф к элементу через функцию.
Так же тут нужно сказать, что у нас будет отсутствовать свойство current и ref будет хранить чистую ссылку на элемент вне объекта
И далее реализуем выделение карточки персонажа через клавиатуру и навешивание на него стилей
src > components > charList > CharList.js
Итог: мы имеем выделение персонажей при клике
014 Порталы
Так выглядит стартовое приложение:
Порталы позволяют отрендерить определённые элементы вне своего родительского компонента
Конкретно сейчас мы имеем выскакивающее модальное окно, которое скрывается внутри нашего компонента
Вынесем модальное окно в отдельный компонент
И далее реализуем портал:
он принимает в себя пропс
далее мы создаём ноду, в которой он отрендерится
добавляем ноду в тело страницы
возвращаем созданный портал через метод createPortal, который в себя принимает вёрстку (children) и место вставки (node)
И добавляем портал в вёрстку родителя
Таким образом модальное окно отрендерилось внутри своего компонента, но в другом месте, отдельно от родительского компонента
Так же тут нужно упомянуть, что событие, которое было сгенерировано изнутри портала будет распространяться и на родителя
То есть, если мы на родителя повесим событие onClick и внутри этого же родителя находится портал, то метод будет срабатывать и на дочернем элементе
015 “Бандлинг” и выгрузка проекта на сервер
Для выгрузки сайта на сервер, нужно две вещи:
Имя сайта - домен
Место хранения сайта - хостинг
Хостинг для обычного сайта подойдёт любой. Однако, если мы собираемся заливать веб-приложения, то нам нужны WPS-сервера, которые выделяют нам отдельный виртуальный ПК.
Но чтобы что-то выгрузить на сервер, нужно сначала собрать проект.
npm run eject - позволит получить доступ к настройкам вебпака в create-react-app. Эта операция необратима, поэтому нужно использовать её только в крайних случаях.
npm run build - сбилдит проект, который можно будет выгрузить на сервер
И сейчас мы можем попробовать открыть сбилженный проект
Весь процесс деплоя сайта на сервер можно просмотреть на сайте документации CRA
Чтобы залить приложение на сервер, перейдём в heroku
далее запишем домен
И далее мы можем залить на хироку сайт прямо с гитхаба или через утилиту
И тут нужно оставить примечание, что все зависимости в нашем проекте должны стоять в нужных местах, и если они нужны для работы сайта, то их нужно перенести в dependencies
Устанавливаем утилиту и со страницы деплоя берём команду
И тут мы можем увидеть статус деплоя нашего приложения