Хуки - это функции, которые позволяют заменить функционал реакта в классах для использования внутри функциональных компонентов
Хуки появились в версии 16.8
Пока не существует хуков, реализующих методы жизненного цикла getSnapshotBeforeUpdate, getDerivedStateFromError и componentDidCatch.
2 правила использования хуков:
Хуки можно вызывать только на верхнем уровне - нельзя использовать внутри циклов, условий и вложенных функций
Хуки стоит вызывать только из функциональных компонентов реакта (исключение только одно - это пользовательские хуки)
002 useState
И далее представим два компонента, которые будут рендерить одну и ту же картинку, но будут иметь два разных подхода реализации: функциональный и классовый
Для построения функционального компонента нужно уже будет писать все состояния используя хуки. Конкретно в данном случае пригодится useState()
useState() - это хук, который отвечает за управлением состояниями в приложении. Он возвращает массив из двух элементов: [состояние, функцияУстановкиСостояния]. Для установки нового состояния нельзя мутировать старое и поэтому в функцию нужно передавать стейт + новое изменённое значение
И далее мы имеем два одинаковых компонента на странице:
классовый
функциональный
Вот как выглядит возврат useState()
Если мы попытаемся вызывать функцию установки нового состояния дважды просто через передачу внутрь аргумента, у нас сработают обе функции асинхронно. Это приведёт к тому, что значение состояния будет меняться ровно один раз, потому что оба аргумента (состояние) функции ссылаются на одно и то же значение
То есть тут произойдёт увеличение состояния slide ровно на 1
Однако при таком подходе, когда мы изменяем состояние через колбэк-функцию, стейт будет меняться два раза подряд и при передаче аргумента 1 стейт увеличится на 2
Так же мы можем вынести несколько состояний в одну переменную и хранить в ней объект с несколькими значениями.
Особенность заключается в том, что в отличие от классов объекты автоматически не складываются
this.setState(({ slide }) => ({ slide: slide + i })) - в классах будет работать и свойство autoplay не потеряется
setState((state) => ({ ...state, slide: state.slide + value })) - нужно использовать в функциях деструктуризацию, потому что состояния в них иммутабельны и нужно вставлять полностью новые значения
Так же, если мы передадим в качестве начального значения состояния функцию, то она вызовется ровно один раз - при сборке компонента
Так же будет себя вести функция, если мы передадим колбэк-функцию
Если функцию просто вызвать внутри установки стейта, то она будет вызваться каждый раз при перерендере
003 useEffect
useEffect() - это хук, который выполняет эффекты на определённых этапах жизненного состояния компонента
Побочными действиями (эффектами) обычно являются:
дозагрузка данных
использования сторонних модулей
запуск таймаутов
логирование
изменение ДОМ-структуры
И далее нам нужно обновлять заголовок страницы в зависимости от состояния слайда
И тут показан пример использования хуков жизненного состояния componentDidMount и componentDidUpdate в классовом компоненте. Основная проблема такого подхода заключается в повторении кода.
И в функциональном компоненте эту же самую операцию выполняет одна функция useEffect()
И теперь оба компонента выполняют свои функции одинаково
И так же нужно сказать про разные состояния работы хука useEffect
При такой записи эффект будет выполняться каждый раз при обновлении компонента.
Дело заключается в том, что так функция будет работать как componentDidMount и функция будет меняться при каждом рендере, то есть каждый раз будет создаваться новая функция (которая передаётся в useEffect). Реализован так хук, чтобы не было багов с замыканием, чтобы получать актуальную переменную из состояния.
Такая запись хука будет вызвать срабатывание функции при любом изменении стейта на странице, что не очень хороший подход, так как у нас есть и другие стейты на странице, которые не относятся к выполнению данной функции
Чтобы исправить проблему выше, можно передать второй аргумент в хук - зависимости, которые будут триггерить срабатывание функции
И теперь вызываться функция будет только при изменении целевого состояния
Если нам нужно сэмулировать работу функции componentDidMount, тогда нам нужно передать пустой массив зависимостей, что вызовет срабатывание функции только один раз - при загрузке
Так же мы можем создать несколько хуков useEffect.Желательно создавать отдельные эффекты на каждое действие.
И далее рассмотрим поведение, когда нам нужно реализовать поведение componentWillUnmount, когда при размонтировании компонента нам нужно произвести все отписки (отключить листенеры и таймауты)
Чтобы выполнить данную операцию, нужно просто из эффекта вернуть другую функцию, которая выполнит заданную операцию при размонтировании
Добавим в родительский компонент возможность удалить компонент со страницы
И теперь при каждом монтировании компонента появляется уведомление
004 useCallback
Представим такую ситуацию: нам нужно получать изображения со стороннего ресурса
Но теперь перед нами встаёт проблема, что после каждого изменения стейта, у нас вызывается функция отправки запроса на сервер
И тут на помощь к нам приходит хук useCallback(). Он принимает в себя функцию и мемоизирует ссылку на неё. Функция будет вызываться заново только после того, как у нас поменяется значение зависимости.
Чтобы правильно воспользоваться функцией, нужно создать новый компонент, который будет иметь своё состояние. Внутри неё нужно и отображать те изменения, данные для которых возвращает useCallback().
Конкретно тут мы через useCallback реализовали возврат новой ссылки на функцию, если изменится состояние слайда (если слайд не будет меняться, то ссылаться хук будет на старую версию функции, которая закэширована)
И теперь каждый раз мы будем вызвать мемоизированную функцию. При перезагрузке страницы те изображения, которые возвращает функция, будут закешированы в браузере.
005 useMemo
useMemo() - это хук, который возвращает мемоизированное значение (результат вычислений запоминается в кеше)
И далее мы реализовали функцию подсчёта суммарного количества слайдов через countTotal()
Основная проблема, которую мы сейчас имеем - функция вызывается ещё раз даже когда мы мы меняем совсем не связанное с ней состояние. Если операция подсчёта была бы очень ресурсозатратной, то производительность бы сильно упала
И чтобы исправить вышеописанную ситуацию, можно воспользоваться хуком useMemo(). Этот хук принимает первым аргументом функцию, а вторым аргументом зависимости, при изменении которых будет пересчитываться значение.
Данный хук используется для запоминания значения, которое возвращает функция, чтобы не выполнять тяжёлые операции пересчёта каждый раз.
Если передать пустой массив зависимостей, то значение посчитается ровно один раз.
Представим такую ситуацию, что нам нужно менять стили при определённых условиях у объекта.
Если выводить в консоль уведомление об изменении стилей, то можно увидеть, что при смене любого состояния у нас переприсваивается значение свойств в компоненте. Это происходит потому, что при обновлении компонента у нас будет в переменную style заноситься новый объект, от чего и будет срабатывать useEffect.
Чтобы исправить данную ситуацию, нужно занести объект в useMemo, что сохранит значение переменной в кеше браузера и внутри компонента объект не будет обновляться каждый раз и не будет вызвать срабатывание хука useEffect
006 useRef
useRef() - это хук, который предоставляет прямой доступ к ДОМ-элементам на странице
Создаём переменную, которая будет хранить ссылку на нужный нам элемент
Передаём реф в элемент ДОМ-дерева
Вызываем срабатывание функции
Далее представленный ниже код выполняет:
при изменении состояния (внутри первого инпута) useEffect выводит в консоль значение свойства current у рефа
при клике по текстэрии, значение в рефе будет увеличиваться на 1
при дальнейшем вводе в первый инпут будет выводиться значение рефа, которое уже было увеличено
Таким образом мы сохранили динамическое значение внутри свойства current, которое будет изменяться без перерендера компонента
И при вынесении изменения свойства в эффект, можно увидеть, что компонент не перерендеривается каждый раз, а просто увеличивает свойство, находящееся в рефе
Так же мы можем использовать реф для сохранения предыдущего состояния компонента. Конкретно тут после записи в первый input его прошлое состояние переносится в textarea
007 Практика. Перепишем весь проект на хуки
Далее можно попробовать перевести весь старый проект с классов на хуки
Основной компонент персонажа:
components > app > App.js
components > CharList > CharList.js
Тут нужно упомянуть, что мы используем стрелочную функцию выше её инициализации в коде. Это будет работать, так как useEffect срабатывает уже после рендера компонента
Так же такой короткой записью можно показать, какой аргумент получает функция (newItemLoading) и что она вернёт наружу (false).
Стрелочные функции используются для того, чтобы асинхронное выполнение установки состояния перевести в синхронное и выполнять по порядку. Так же использование колбэк-функции позволяет воспользоваться значением прошлого состояния
Перевод рефов будет чуть более сложным. Когда мы работаем с хуками, то у нас появляется свойство current, в которое и нужно заносить значения хука. Просто так добавить в реф ссылку не получится.
Для этого инициализируем useRef, передаём в него массив, обращаемся в методе фокуса к свойству current
И далее уже в самом рендере будет вызываться функция, которая сформирует массив ссылок на те элементы, которые сгенерируются внутри мапы
Дальше идёт компонент информации о персонаже:
components > CharInfo > CharInfo.js
И компонент рандомного персонажа:
components > RandomChar > RandomChar.js
008 Создание собственных хуков
И сейчас мы добавили в работу ещё одно поле для ввода текста - код был повторён.
Тут мы сталкиваемся с такой ситуацией, что мы постоянно повторяем код, который написали единожды
И теперь мы можем выделить всю вышеописанную повторяемую логику в отдельный хук. Кастомный хук - это механизм повторого использования логики с состоянием.
Таким образом выглядит классический кастомный хук. Обычно он возвращает несколько объектов в массиве и сохраняет в себе определённую логику
Либо мы можем собрать всю логику хука в отдельную переменную, если возвращать из него объект. Такой подход позволит создать несколько независимых объектов
Пример использования хука внутри компонента:
Инициализируем хук два раза для двух наших элементов формы
Мы не передаём внутрь функции validateInput текст (переменная colorInput), так как он берётся из внутреннего состояния хука, который относится к данному инпуту
В элементы мы передаём value и onChange, которые относятся к инкапсулированной логике их хуков
Создадим хук, который будет получать данные о персонажах с сервера и выдавать их. Так же он будет контролировать состояние загрузки и ошибки
src > hooks > http.hook.js
Далее нужно переделать сервис по общению с сервером под хуки и встроить в его запросы request из прошлого хука
src > services > marvel.service.js
Далее нужно поменять общение с сервером в остальных компонентах.
Чтобы всё работало нормально, нужно так же правильно вставить clearError.
src > component > randomChar > RandomChar.js
Для правильного перерендера объектов карточек, нужно убрать использование переменной content и использовать просто items, который у нас генерирует другая функция.
src > component > charList > CharList.js
В CharInfo нужно просто поменять общение с сервером на хуковый и добавить очистку ошибки, если сервер не сможет вернуть данные (чтобы в принципе информация обновлялась)
src > component > charInfo > CharInfo.js
010 Что такое batching и как он работает в React 18+
==Batching== - это объединение обновления нескольких состояний в одну операцию для улучшения производительности.
Объединение нескольких обновлений позволяет экономить ресурсы ПК за счёт единоразового перерендера
У нас есть функция, которая вызывает внутри себя срабатывание изменения двух разных состояний, то есть компонент должен перерендерится два раза и вывести два лога в консоль
Однако при каждом срабатывании функции и изменении двух состояний, мы получаем только один лог в консоль - обе эти операции совмещаются
Но если поместить вызов изменения внутрь колбэка (то есть изменения происходят асинхронно), то мы будем получать два лога в консоль, так как эти состояния будут меняться отдельно друг от друга и каждый раз обновлять компонент
Так же если запихнуть log() в компонент CharList, то в консоли выведется сразу несколько таких логов, так как наши изменения состояний находятся в асинхронных функциях
Однако тут используется версия React 18, в котором оптимизированы некоторые изменения состояний и их бэтчинг, что сокращает количество перерендеров
Однако, если нам нужно разъединить операции обновления, то мы можем воспользоваться функцией flushSync (эту функцию нужно использовать отдельно на каждую операцию)
011 (д) useTransition, useDeferredValue и другие нововведения React 18+
useId() - генерирует уникальный идентификатор (он не должен использоваться для формирования атрибута key)
Так же были добавлены хуки для интеграции сторонних библиотек:
Конкурентный режим — это нововведение в React. Его задача — адаптировать приложение к разным устройствам и скорости сети. Пока что Concurrent Mode — эксперимент, который может быть изменён разработчиками библиотеки, а значит, новых инструментов нет в стабильной версии.
Сам конкурентный режим может ставить на рендер сразу несколько компонентов или ставить их на паузу определяя приоритет
И сейчас мы столкнулись с такой проблемой, что ввод в инпут очень сильно лагает. Дело в том, что наш стейт меняется сразу при вводе новых данных, что тормозит ввод новых символов из-за постоянного рендера
useDeferredValue() - данный хук позволит получить нужное нам значение с небольшим интервалом, чтобы задержать рендер компонента
useTransition() - так же позволяет задержать перерендер компонента, но предоставляет возможность самому указать, что будет в интервале и как на него реагировать
Хук возвращает нам булеан ожидания рендера isPending и функцию, в которой будет находиться функция, которая выполняется длительное время. Далее нам нужно будет только сделать условный рендеринг, куда мы вставим спиннер (или другой элемент для ожидания загрузки)
И тут появляется элемент загрузки
012 Навигация в приложении, React Router v5+
Сейчас имеется сразу несколько версий реакт-роутер-дома, но стоит начать с пятой
Устанавливаем нужную нам версию через @версия
И далее нам нужно закинуть в проект три компонента из роутер-дома:
BrowserRouter - роутер по всем страницам приложения (отслеживает переход по ссылкам)
Route - отдельный роут приложения
Switch
И далее обернём все наши страницы подобным образом:
В BrowserRouter (который переименовали в Router) поместим всё наше приложение
А в отдельный Route поместим компоненты, которые должны рендериться на отдельной странице
components > app > App.js
Но тут стоит заметить, что роутер компонует между собой элементы с ссылками, которые хранят одинаковые значения. То есть в данном случае первый роут и второй объединятся, так как / и /comics имеют при себе слеш
И при переходе на /comics у нас будет следующая картина:
Такой подход был бы уместен, если мы заранее спланировали бы вёрстку таким образом, что нам нужно показывать дополнительные данные (тыкаем по карточке товара и раскрывается его расширенное описание)
Switch - переключатель по роутам - он уже загружает отдельный роут как новую страницу
Однако тут мы упираемся в такую особенность работы свича, что он грузит только первую ссылку, которая совпадает с url, имеющейся на странице
То есть свитч грузит только первую страницу, которая совпала с первой ссылкой (которая всегда /) и не смотрит на следующие ссылки, которые имеют тот же маршрут
Чтобы исправить проблему, у нас есть два пути решения:
Главную страницу / расположить в конце списка свича
Добавить атрибут exact, который обязует, чтобы рендер был только по написанию полного и правильного пути
Вот пример использования первого подхода (все /имя нужно будет писать до / главной страницы)
И вот пример использования обязующего атрибута
Результат:
И далее, чтобы добавить ссылки в наш проект, нужно в нужное место в компоненте добавить компонент Link, который в качестве ссылки в себя принимает атрибут to
components > appHeader > AppHeader.js
И сейчас ссылки для перехода по страницам работают
Так же мы имеем функцию redirect, которая при определённых условиях позволяет заредиректить пользователя (например, если он не залогинен или определённой ссылки не существует)
Так же мы имеем атрибут NavLink, который позволяет нам стилизовать активную ссылку. Его особенностью является наличие атрибута activeStyle
Однако, когда мы добавляем стили для наших элементов, стоит добавлять атрибут exact, чтобы стили применялись не ко всем элементам сразу, а только к нужным
components > appHeader > AppHeader.js
И далее мы можем вынести страницы в отдельные компоненты и поместить их в папку pages
src > components > pages > ComicsList.js
src > components > pages > MainPage.js
А далее экспортировать их через index.js, который сократит до них путь
src > components > pages > index.js
И тут используем импорт
src > components > app > App.js
013 React Router v6+
И теперь нужно установить последнюю версию роутера
Тут находится руководство о переходе с пятой версии на шестую
Вместо компонента Switch используется компонент Routes.
Нужный компонент для отрисовки теперь передаётся не в качестве child, а передаётся внутрь атрибута element.
Теперь вместо хука useHistory нужно использовать useNavigate
Теперь мы пишем не так:
А так:
Хук useRouteMatch заменили на useMatch
Компонент Prompt больше не поддерживается
Так же нужно сказать, что такого атрибута как exact теперь не существует. Внутри Routes проходит правильное сравнение ссылок, что не приводит к рендеру одного компонента внутри другого. Если нам нужно будет использовать эквивалент этому атрибуту в NavLink, то там мы вместо него пишем end
components > app > App.js
Так же в новой версии у нас пропала наша классическая композиция, когда у нас свитч рендерил сразу все страницы, если их не разделять атрибутом exact. Чтобы использовать данную функциональность и подгружать другую страницу внутри нашей страницы, нужно использовать компонент <Outlet />. Он загрузит другой компонент на нашей странице при клике на нужную ссылку.
Так же нужно указать, что ссылки внутри роутов будут относиться к этим роутам. То есть, если родительский роут имеет ссылку to='/comics', то при выборе внутри него ссылки to='/deadpool' мы перейдём по ссылке /comics/deadpool. В пятой версии с этим были определённые трудности.
Из вышеописанных исправлений вытекает дополнительный функционал:
to='.' будет осуществлять переход на эту же страницу
to='..' будет вызывать страницу на один уровень выше (родительскую)
to='../bayonette' выйдет на уровень выше и перейдёт оттуда на другую страницу (которая находится в родительском компоненте)
Так же из компонента NavLink удалили атрибуты activeStyle и activeClassName. Вместо них нужно самому делать функции по добавлению нужного функционала
Исправим хедер страницы, чтобы он поддерживал 6 версию роутер-дома:
заменяем exact на end
заменяем activeStyle на style. Сам же стиль будет автоматически принимать в себя аргумент активности (isActive), чтобы мы могли навесить нужные нам стили
Теперь применение стилей правильно работает:
014 Практика создания динамических путей
Страница ошибки
Динамические страницы
Дальше используется Router Dom v5
Match хранит в себе данные о том, как именно path совпал с текущим адресом
History представляет из себя API для организации перехода между страницами
Location хранит в себе состояние положения роутера
Для работы с данными объектами используются хуки: useParams, useHistory, useLocation
Для реализации побочной ссылки, которая будет грузиться изнутри другого роута, нужно добавить новый роут с родительским путём и указать дополнительный динамический путь через :. То есть ссылка будет выглядеть следующим образом: /comics/:comicId - передаём параметр comicId в динамическую ссылку
components > app > App.js
В сервисе нужно иметь функцию, которая будет по id возвращать нужный нам комикс
service > marvel.service.js
Далее в компоненте списка комиксов поменяем ссылку a на Link и в параметры ссылки передадим item.id, который будет ссылаться на определённый комикс
components > comicsList > ComicsList.js
Далее нам нужно реализовать страницу отдельного комикса
components > pages > SingleComicPage.js
Экспортируем страницу одиночного комикса
components > pages > index.js
Чтобы слово Comics горело даже в отдельном комиксе, нужно убрать строгое сравнение ссылки через exact из компонента AppHeader
components > appHeader > AppHeader.js
015 Динамические импорты и React.lazy
На определённом этапе разработки приложение станет настолько большим, что уже оно начнёт загружаться крайне длительное время. Но мы можем определить, какие участки приложения нам не нужны на этапе первичной загрузки и так же мы можем указать с помощью JS на эти блоки кода.
Для примера создадим функцию логгера:
components > charList > someFunc.js
Динамический импорт возвращает промис с объектом модуля
Тут нужно напомнить, что любой экспорт из файла в JS экспортирует единый объект (в данном случае - obj), который хранит данную функцию в качестве свойства (obj.logger). Если мы экспортируем по умолчанию через export default, то на выходе мы получаем объект с одним свойством - obj.default
Так же обязательно всегда нужно указывать catch, который будет срабатывать, когда не сработал импорт / неправильно был указан путь
components > charList > CharList.js
Но зачастую используется более простой синтаксис - получение нужной функции через деструктуризацию
components > charList > CharList.js
И если нам нужно будет вытащить дефолтную функцию, то в импортах нужно будет обратиться не к функции по имени, а к свойству default, которое содержит функцию
components > charList > CharList.js
Далее переходим к функционалу реакта - React.lazy
Основным условием является то, что компонент должен экспортироваться дефолтно из файла
Так же все динамические импорты нужно вставлять после статических, иначе может произойти ошибка
Далее нужно как и с промисами обработать возможную ошибку. Для этого предназначен дополнительный компонент Suspense. Он принимает в себя атрибут fallback, который будет показываться пока подгружается нужный нам компонент из динамического импорта
components > app > App.js
И теперь во время загрузки этой страницы у нас будет показываться спиннер пока не загрузится ошибка
Так же можно сделать подобную подгрузку для всех страниц, чтобы они не грузились сразу все пользователю, а только по надобности
И уже таким образом будет выглядеть ошибка, если ленивые импорты вставить внутрь статических
До ленивых импортов мы имели 3 файла JS и вся папка весила 751 килобайт. Тут нужно сказать, что все эти файлы пользователь подгружал сразу, даже если их функционал ему не нужен был
После lazy-импорта количество файлов возросло в несколько раз и вес папки со скриптами вырос до 880 килобайт. Хоть скрипты и весят больше в общем, но теперь пользователь не будет скачивать все страницы сразу - он будет получать только актуальные ему страницы и подгружать их в процессе использования приложения.
Использовать ленивую загрузку стоит уже в больших приложениях, где первая скорость загрузки уже будет переваливать за 3 секунды
Зачастую такую загрузку применяют уже к целым страницам приложения
Но таким же образом можно выносить в ленивую загрузку и отдельные компоненты
016 React.memo, Pure Component и оптимизация скорости работы приложения
React.memo - это компонент высшего порядка (HOC), который предназначен для мемоизации рендера компонента. Если в компонент не пришли новые пропсы или не изменился стейт, то компонент не перерендерится и сохранит ресурсы компьютера пользователя.
Например, мы имеем форму. При нажатии на кнопку компонент формы получает новые пропсы и перерендеривается.
Чтобы решить вышеописанную проблему, нужно просто обернуть компонент в memo(), который сохранит результат рендера и при неизменных значениях не будет рендерить компонент заново
Однако тут нужно упомянуть, что перерендер будет происходить, если внутри пропсов мы передаём свойства с вложенными объектами
Чтобы указать, что в приложении нам нужно реализовать для определённого пропса глубокую проверку (проверять изменение вложенных значений), то нам нужно реализовать функцию, которая будет сравнивать значения и возвращать boolean
React.PureComponent - это расширение классовых компонентов, которое в отличе от Component триггерит каждый раз функцию shouldComponentUpdate(), которым мы определяем потребность в обновлении компонента
Всю логику, что мы реализовывали через memo данный компонент реализует сам. Однако нам нужно будет так же проводить сравнение внутри shouldComponentUpdate(), если мы будем передавать вложенные объекты
Если мы хотим контролировать перерендер в обычном компоненте, который наследуется от Component, то нам нужно будет использовать функцию shouldComponentUpdate()
Тут стоит сделать пометку, что логику очень глубокого сравнения делать не стоит
Вывод:
memo() используется для функциональных компонентов
Для классовых компонентов используется PureComponent или Component вместе с функцией shouldComponentUpdate()
Используется мемоизация для компонентов, которые часто получают одинаковые пропсы
Если добавить мемоизацию на компонент, который постоянно получает новые пропсы, то можно только затормозить работу приложения дополнительным сохранением данных - поэтому использовать данный функционал нужно аккуратно
Например, если мы передадим функцию, то компонент даже с memo() будет перерендериваться постоянно из-за того, что каждый раз при передаче будет создаваться новая функция (а функция является объектом в JS).
Это так же будет происходить, если мы вынесем эту функцию в отдельную именованную стрелочную, потому что наш компонент App перерендеривается каждый раз, когда в нём меняется состояние и из-за этого пересоздаётся функция внутри него
Чтобы закешировать функцию и не пересоздавать её по-новой каждый раз, можно просто замемоизировать её через useCallback()
017 React Context и useContext
React.createContext и useContext - это функциональность, которая позволит создать один глобальный провайдер пропсов, чтобы пользоваться ими из любого участка приложения. То есть мы можем передавать данные по дереву компонентов не прибегая к property drill (сверлим компоненты пропсами, которые нужно передать ниже)
Вот пример антипаттерна передачи пропсов через несколько промежуточных компонентов:
Функция createContext создаёт единый контекст в приложении
В эту функцию можно передать дефолтное значение, которое будет передаваться во все провайдеры, если в них не было передано значение в атрибут value
Сам Provider является компонентом и в себя он принимает любое значение своего компонента (компонент App раздаёт состояние data)
Consumer так же является компонентом, который получает все данные из провайдера. Данный компонент получает функцию с данными в виде одного аргумента и через неё и можно отрендерить внутренности (просто так вставить компонент не получится)
Все компоненты, которые используют данные провайдера будут обновлены при изменении этих данных
Сам же объект, который располагается в контексте хранит в себе:
Переданные данные
Provider - раздаёт данные всем компонентом из единого места в приложении
Consumer - подписывается на провайдера и следит за изменением его данных
Так же есть более простой метод получать данные из состояний - это присвоение контекста компоненту
Так же можно использовать static присвоение контекста, но это экспериментальный способ
Ну и работа заметно упрощается, когда мы работаем с функциональными компонентами и просто используем useContext()
так же приложение можно разбить на модули с контекстом
контекстов может быть несколько в приложении
провайдеры можно вкладывать в провайдеры, чтобы добавлять дополнительно контекст
Таким образом можно изменить состояние через функцию, переданную внутри контекста
Теперь нам везде так же нужно передавать функцию forceChangeMail (меняет стейт), чтобы не было ошибок
Так же нужно сказать, что если мы в Provider не передаём атрибут value, то он будет равняться undefind, что вызовет ошибки
Однако, если убрать провайдера, то мы будем получать данные по умолчанию из контекста
И чтобы не получать ошибок, нужно добавлять все новые значения в значения по умолчанию контекста
Так же в провайдера не стоит передавать объекты напрямую, так как такая запись будет ухудшать оптимизацию проекта
018 useReducer
useReducer - это функция, которая управляет ограниченным набором состояний. Она заменяет useState и позволяет нам предсказывать определённые наборы состояний компонента.
Хук возвращает само состояние и функцию dispatch, которая вызывает изменение состояния.
dispatch принимает в себя объект с одним обязательным свойством type, которое хранит в себе тип операции
Хук принимает в себя три аргумента:
Функцию-reducer, которая отвечает за изменение состояния
Начальное состояние
Ленивое создание начального состояния
И тут у нас построена определённая структура:
Добавлен хук useReducer, который будет контролировать состояние автоплея слайдера
Внутрь мы передаём функцию reducer и начальное значение состояния
Функция reducer через switch-конструкцию возвращает определённое значение в зависимости от переданного значения action. Так же эта функция принимает в себя state, чтобы от него иметь возможность поменять состояние
Далее мы вызываем работу useReducer из вёрстки через функцию dispatch, которая принимает в себя экшен. Этот экшен уже будет передан в функцию reducer
И тут уже можно увидеть подконтрольное изменение состояния, когда мы уже знаем, что будет в качестве значения состояния. Для этого и предназначен данный хук
Так выглядит ленивое задание начального значения через третий аргумент хука редьюсера:
При таком подходе у нас уже изначально стоит правильное значение в состоянии, которое соответствует будущим объектам
Так же в dispatch наряду с type обычно передают второе свойство - payload. Оно хранит в себе кастомное значение, которое мы хотим передать в состояние.
019 Компоненты высшего порядка (HOC)
Первым делом нужно посмотреть на то, какой механизм отвечает за логику работы ХОКов.
За них отвечает подобная логика, когда у нас вызывается и возвращается одна функция внутри другой. Каждая из этих функций обогащает друг друга.
Так же можно будет работать и с классовыми компонентами
ХОКи могут пригодиться, когда нам нужно обогатить функционал достаточно похожей логики. Например, нам нужно вывести список товаров для клиента на сайте и для администратора внутри административной панели - это один и тот же список, но для разных пользователей он будет иметь немного разную информацию (конкретно администратор сможет каждый компонент изменить или удалить).
В изначальном варианте у нас представлена логика, когда мы получаем информацию о том, на каком слайде мы находимся, с условного “сервера”. Из-за определённых ограничений мы не можем использовать одну эту функцию в обоих слайдерах.
Все ХОКи начинаются с with.
Конкретно тут ХОК оставил страницу ровно такой же, но он сохранил в себе обобщённую логику из двух данных компонентов и передал её в них обратно.
Так же есть второй вариант создания ХОКа, когда мы создаём функцию, которая вызывает создание функции, возвращающей компонент из переданного пропса в первую функцию.
Конкретно тут ХОК обогащает переданный компонент в него логгером при рендере на странице
И теперь компонент приветствия вызывает лог в консоли
Итог:
Когда не стоит использовать HOC:
Если компоненты слишком разные и их логику не получается обобщить. Самый идеальный вариант, когда мы передаём минимальное количество пропсов в возвращаемый компонент:
Если в проекте имеется только один компонент, который подходит для использования вместе с компонентом высшего порядка
Если приходится каждый раз модифицировать HOC при подключении нового компонента
Когда использовать:
Когда много компонентов имеют схожую логику выполнения
Когда понятно, что ХОК не будет расти со временем из-за схожести логики
Когда нужно добавить общую логику для выполнения самых разных компонентов
020 Библиотеки и экосистема React
Современная разработка веб-приложений не представляется без использования сторонних библиотек: нужно быстро выполнить задачу, нужно выполнить задачу на уровне, хочется просто не придумывать велосипед - за всем этим можно обратиться к уже готовым библиотекам
Полезные ссылки, на которых можно узнать побольше о библиотеках реакта:
Тут находится список из 10 библиотек, которые позволяет заменить простой функционал реакта
Тут находятся полезные хуки, которые можно использовать в любом проекте
Тут находится информация по всей актуальной экосистеме реакта
021 React Transition Group
React Transition Group - классический модуль для создания анимаций в React.
Компонент Transition. Он принимает в себя на вход nodeRef (элемент ссылки), in (элемент появляется или исчезает со страницы) и timeout (длительность анимации)
Элемент при появлении делится на три этапа (пропс in в позиции false, что говорит об отсутствии элемента):
onEnter - инициализация появления
onEntering - появление
onEntered - окончание появления на странице
Противоположные состояния имеются, при наличии элемента на странице (актуальна анимация исчезновения элемента со страницы)
Все этапы изображены тут:
Все промежутки ожидания имеют определённую длительность. Анимировать мы можем переход от entering к entered и от exiting к exited
Тут стоит отметить, что свойство display (none и block) невозможно анимировать, поэтому их не используем
Так же в транзишене есть атрибут, который размонтирует компонент, когда его нет на странице
Так же стоит рассказать, что для транзишена ещё имеются и функции, которые позволяют выполнять разные операции на разных этапах анимации компонента.
Например, onEntering принимает в себя функцию, которая может выполниться во время появления компонента
Тут показан жизненный цикл выполнения данных функций:
Например, нам нужно скрыть кнопку, которая является триггером для модального окна
Далее идёт компонент CSSTransition. Его отличительная особенность заключается в том, что мы не должны передавать состояния внутрь компонента и рендерить весь компонент через функцию тоже не нужно. Этот компонент работает с готовыми стилями и производит анимацию на её основе.
Данный компонент принимает в себя атрибут classNames, где указывается начальное наименование стилей, которые относятся к этому компоненту и далее они воспроизводятся в зависимости от их префикса:
-enter
-enter-active
-exit
-exit-active
Далее идут два компоненты - SwitchTransition и TransitionGroup - это компоненты, которые модифицируют поведение первых двух
Основной особенностью SwitchTransition является переключение режимов анимации через атрибут mode:
out-in - запускает анимацию и дожидаётся её окончания перед тем, как запустить анимацию другого компонента
in-out - сначала дожидается анимации появления второго компонента и только потом удаляет первый компонент со страницы
Компонент TransitionGroup занимается оборачиванием других компонентов анимации.
Конкретно в этом компоненте обычно разворачивают остальные компоненты транзишена из массива. Так же он позволяет не указывать атрибут in, так как этот компонент отслеживает начало всех анимаций.
022 Formik, Yup и работа с формами любой сложности
Formik - это популярная библиотека под React по работе с формами на странице.
initialValue - важный начальный атрибут для компонента Formik. В него мы вписываем связанные имена с инпутами в виде ключей и их данные являются начальными значениями. Связь устанавливается с формами через атрибут внутри формы name или id
validate - атрибут, который хранит функцию валидации форм
onSubmit - атрибут, который хранит функцию, срабатывающую при отправке формы
Тут представлен пример классического использования формика на странице:
Так же формик предоставляет готовые компоненты Form, Field, ErrorMessage, которые можно использовать в проекте вместо стандартных форм:
Далее будет реализована форма отправки данных на пожертвования. Со всей формы будут собираться данные и выводиться в логе строковый вариант объекта
функция handleChange будет перехватывать изменения внутри формы и определять, в какой произошли изменения
функция формика handleBlur записывает формы в объект touched, который передаётся в форму внутри объекта values. После добавления этой функции в инпут, можно будет воспользоваться объектом touched для проверки при выводе ошибки
При вводе валидных данных форма будет их собирать и отправлять
Все формы могут воспринимать ошибки и реагируют на них
У нас выделяется всего один инпут с ошибкой (а не сразу во всех отображается ошибка, как бы было без touched)
Так же данные не будут отправляться, если в поле есть невалидные данные
Самый простой вариант валидации - это использовать стороннюю библиотеку, которая поможет избежать рутинных процессов, а именно - Yup. Он уже имеет много методов для валидации данных в себе и очень прост в использовании.
Тут показан пример, заданный в validationSchema (функция валидации данных находится второй по списку!). Все значения описаны в объекте схемы и через чейн вызваются функции проверки данных с формы. Сам Юп возвращает один объект ошибки, который работает подобно нашей самостоятельной реализации выше
И так выглядят все реакции на ошибки в форме:
Так же вместо того, чтобы писать везде одинаковые атрибуты, можно просто вставлять деструктурированный вызов функции getFieldProps('имя_поля'), который вернёт все нужные атрибуты в инпут
Далее, чтобы использовать компоненты самого формика, нужно будет переписать код на классическое поведение без хука
Нам нужно будет удалить все использования переменной formik
Перенести все данные из хука в компонент Formik
Убрать хук
Убрать все лишние атрибуты из инпутов
Переименовать инпуты в Field
Вместо условных конструкций с выводом ошибки написать вывод через ErrorMessage
В итоге мы имеем ровно такую же форму, но более оптимизированную по коду
Когда мы говорим про чистую работу с формиком, то он сам подставляет все нужные значения в формы, которые мы обозначили как Field. Сам Field - это общее поле, которое можно через атрибут as указать как другое поле (селект или текстэриа)
Так же и с полем ошибки - использовать готовый компонент ErrorMessage куда более простой и быстрый вариант. Оно в себя принимает:
name - имя поля, к которому привязывается ошибка
component - тег, которым оно отрендерится на странице
Так же есть другой вариант отобразить ошибку - расположить функцию по отрисовке внутри компонента
Ну и так же формик предоставляет хук useField, который позволяет нам сделать свои шаблоны под повторяющиеся формы на странице. Сам хук в себя принимает пропсы, а возвращает кортеж из значений:
field - хранит в себе пропсы формика (включая события onChange, onBlur и onValue)
meta - хранит метаданные с ошибками и был ли использован данный инпут
024 Разбор домашнего задания
Первым делом, нужно добавить метод, который достанет одного персонажа с сервера по имени
service > MarvelService.js
Далее добавим компонент поиска персонажа
components > CharSearchForm > CharSearchForm.js
Тут добавим компонент поиска персонажа на страницу под информацией о выбранном персонаже из списка
components > pages > MainPage.js
Тут будет содержаться логика поведения страницы для комикса или персонажа
Таким образом выглядят лейауты со стилями в страницах:
И финальная часть. Уже компонент App определяет то, какая страница у нас загружается - персонаж или комикс. Тут в компонент передаётся dataType, который определяет запрос свичч-конструкции
component > app > App.js
Итог: мы имеем форму поиска, которая реагирует на действия пользователя и предоставляет возможность перейти на страницу по персонажу
025 SEO-оптимизация веб-приложений, React-helmet
SEO - Search Engine Optimization - это отрасль оптимизации поисковых запросов за счёт выполнения сайтом определённых требований
Основные показатели, влияющие на СЕО положительно:
Валидность вёрстки
Использования семантической вёрстки и валидность тегов
Скорость загрузки
Заполнены для каждой страницы правильно метатеги и тайтл (они будут отображаться в поиске), а так же использование OG-тегов
Основной проблемой современных SPA является то, что они не отображают никакого контента, даже когда на них зайдёт робот (он видит только пустой div), что приводит к снижению СЕО-оптимизации.
Обычно, чтобы бороться с такой проблемой, используют фреймворки с SSR (рендерингом страницы на стороне сервера), который сразу отдаёт отрендеренную страницу любому пользователю или роботу. Самый популярный из имеющихся - NextJS. Он хранит в себе все возможности для оптимизации страницы (сам конвертирует изображения, предоставляет роутинг, рендеринг на сервере, общение с сервером через пропсы и даёт настроить метатеги на всех страницах)
Однако подход с SSR требует много вычислительных ресурсов, что приводит к сильной нагрузке на сервера. Поэтому обычно используется пререндеринг страницы через тот же react-snap, который будет отдавать боту готовую страницу
Чтобы настроить метатеги на странице можно воспользоваться модулем react-helmet, который будет работать как на клиенте, так и на сервере
Добавление мета-тегов на страницу выглядит просто:
Добавляем тег Helmet в компонент
Внутрь него вставляем нужные мета-теги либо можем передавать их в качестве атрибутов компонента
Вставим мету на страницу со списком комиксов
components > pages > ComicsPage.js
Вставим мету на страницу комиксов
components > pages > SingleComicLayout.js
Вставим мету на страницу персонажей
components > pages > SingleCharacterLayout.js
И так же можно убрать мета-теги из хтмлки
index.html
И теперь на всех страницах имеются свои мета-теги
026 Принцип конечного автомата (FSM, Finite-state machine) и +1 подход к состояниям
Начнём с того, что в приведённом примере ниже, отображение на странице зависит от данных четырёх состояний. Это очень громоздкая и зачастую непонятная конструкция, но ей пользуются для реализации вёрстки на страницах.
И тут к нам приходит Концепция Конечного Автомата (FSM - Finite-State Machine - State Machine - Машина состояний). Это такая сущность (математическая модель), которая имеет определённое количество состояний. То есть данная сущность может иметь ровно одно состояние в определённый момент времени.
Если кнопка скрыта, то она уже имеет определённое состояние. Она не может быть выключена, так как она скрыта и так далее.
На примере курьера можно привести его состояния:
Ожидание (тут он может отдохнуть, выпить кофе)
Получение заказа (тут он уже уточняет адрес, общается с заказчиком и так далее)
Доставка заказа (уже тут он перемещается к заказчику)
Получение оплаты (а тут получает оплату и дальше переходит в состояние ожидания)
Данная концепция не выделяется как отдельный приём. Оно отвечает лишь за то, что мы контролируем количество состояний, которое может быть в приложении.
Для работы со машиной состояний имеется несколько библиотек под JS (их стоит использовать уже вместе с Redux):
Модифицируем хук, который выполняет получение данных с сервера. В него на каждый из этапов будем устанавливать состояние процесса, который будет вызвать определённый рендер в компонентах
Первым делом, мы можем удалить состояния loading и error из хука и так же установку этих состояний внутри хука
hooks > http.hook.js
Возвращаем хук установки процесса и состояние самого процесса через хук сервиса общения с сервером
service > MarvelService.js
Эта уже сама машина состояний. Тут мы определяем, какие процессы будут выполняться на разных этапах процесса
utils > setContent.js
И далее тут в рендере используем setContent(). Эта функция будет каждый раз перевызываться при изменении состояния process. Так же в этом же компоненте нужно вызывать setProcess после того, как мы получили данные внутри updateChar
component > charInfo > CharInfo.js
Так же стоит переименовать пропс, получаемый в компоненте на тот, что передаётся внутри свича
Тут так же пришлось решать проблему с тем, что в списке во время загрузки отображалось null (это приводит к тому, что мы поднимаемся вверх страницы и заново приходится скроллить вниз по списку). Эту проблему можно было решить только переписав функцию установки контента, где мы в загрузке показываем старый список или спиннер (если старых элементов списка нет)
component > charList > CharList.js
Код мы оптимизировали и компонент информации о персонаже так же работает и список персонажей тоже
Список комиксов так же переводим в стейт-машину
component > comicsList > ComicsList.js
Далее модифицируем компонент рандомного персонажа
component > randomChar > RandomChar.js
И так же можно установить машину состояний для отдельных страниц комиксов и персонажей
components > pages > SinglePage.js
Ну и так же стоит переписать на стейт-машину компонент поиска персонажа
components > charSearchForm > CharSearchForm.js
027 Разбираем ошибки сторонних библиотек и проблему с фокусом
Представленную ошибку выводит модуль react-helmet. Такие ошибки может решить только сам разработчик и реакцию сообщества на данные баги можно найти в обсуждениях на гитхабе, если загуглить ошибку
Далее в проекте появился баг, который возникает из-за подхода с конечными автоматами - у нас выделяется карточка только при втором нажатии
Чтобы найти проблему, можно поставить логгер на предполагаемом месте. После клика на персонажа у нас почему-то перерендеривается вся структура, что и не даёт анимации выделения сработать
Если выйти в компонент родителя, то можно и там оставить логгер, который нам скажет, что при выделении персонажа перерендеривается вся страница
Поэтому одним из вариантов является запоминание результата рендера, если не были изменены никакие другие стейты процесса. В данном случае нам может помочь хук useMemo()