Как стать автором
Обновить
822.21
OTUS
Цифровые навыки от ведущих экспертов

React hooks, как не выстрелить себе в ноги. Разбираемся с замыканиями. Совместное использование хуков

Время на прочтение 7 мин
Количество просмотров 9.8K

По отдельности хуки могут быть относительно понятны, но у многих возникают вопросы, как их использовать вместе. Особенно часто возникают подобные вопросы при переходе от классовых компонентов к функциональным. В этой статье мы разберем несколько случаев:

  1. Как работать с запросами в useEffect

  2. Как предотвращать лишние запросы в useEffect с помощью useRef и использовать useRef как стабильную переменную

Уже этих примеров будет достаточно для многих прочих случаев. Главное обсудим, как ведет себя замыкание в хуках, почему именно так, и почему useRef работает как стабильная переменная.

Запросы и useEffect

Существует множество подходов, как работать с запросами в react. Мы не будем разбираться, какой подход правильный, а какой нет, потому как пока нет общепринятого подхода. Лучше сосредоточимся на том, как технически правильно отправлять запросы из react компонента. Все последующие примеры из этого блока можно найти тут.

Запрос при монтировании компонента

Самый простой вариант, который содержит состояние загрузки, ошибки и данных, выглядит так. Запрос будет отправлен при монтировании компонента.

Давайте разберем подробнее, что здесь происходит.

В строках 11-13 создаются состояния загрузки, оно нам понадобится для отображения индикатора загрузки (24 строка); также состояние ошибки, оно позволит нам обработать ошибку запроса, как например в строке 25; состояние данных, его мы выведем на экран в строке 27;

Сам запрос происходит в useEffect, сработает только при монтировании, о чем нам говорит пустой массив зависимостей в строке 22.

Еще до запроса устанавливаем состояние загрузки true смотри строку 16. На следующей строке отправляем запрос, в строке 18 стандартная обработка json-а, и если все успешно, в строке 19 получим данные и установим их в качестве data состояния. Если случилась ошибка, мы ее отловим в блоке .catch на 20 строке и запишем эту ошибку в соответствующее состояние. Ну и в блоке .finally, который сработает при завершении запроса в любом случае (хоть ошибка, хоть успех), убираем состояние загрузки.

С запросами при монтировании разобрались, теперь давайте обсудим, как отправлять запросы при изменении какого-то параметра.

Запросы при изменении параметра

Бывает нам нужно перезапросить данные, при изменении какого-нибудь параметра. В нашем примере этим параметром будет id записи. Этот id принимает компонент в качестве prop.

В строке 37 записываем id в url запроса и в строке 42 добавляем id в массив зависимостей. Благодаря этому при изменении id будет отправляться новый запрос.

В этом примере есть небольшой недостаток: если id изменится быстро, запрос с предыдущим id может оказаться не завершен, из-за этого сначала состояние компонента изменится в соответствии с результатом предыдущего запроса (установится data/error/loading), а только потом состояние изменится в соответствии с текущим запросом.

Недостаток небольшой, потому как он не приведет с ошибкам, но если нам нужно добиться минимального количества обновлений компонента, в целях улучшения производительность, есть способ предотвратить лишние обновления.

Запросы при изменении параметра с предотвращением лишних обновлений

Чтобы предотвратить лишние обновления компонента при частых запросах, достаточно ввести вспомогательную переменную в самом теле useEffect, в нашем случае это canBeSet переменная (56 строка).

Обращаю внимание, конструкция canBeSet && setLoading(...) аналогична if (canBeSet) setLoading(...). Таким образом, когда canBeSet = false функции изменения состояния просто не выполнятся.

Важно понимать, что та функция, которую мы передаем в useEffect - это не статичная конструкция, которая используется много раз. При каждом обновлении компонента создается новая функция, при этом в памяти хранится предыдущая.

Представьте, монтируется компонент, useEffect сохраняет функцию effect (что передали внутрь него). В ней создается переменная canBeSet = true и далее отправляется запрос. Установка всех состояний компонента сработает только если canBeSet останется true. Но, вдруг, изменился id, срабатывает так называемая функция cleanup, это то, что возвращает функция effect (смотри 64-66 строку), а в этой функции переменная canBeSet устанавливается false. И да, несмотря на то, что при обновлении компонента внутрь useEffect попадает новая функция, в которой есть canBeSet переменная, в памяти все еще существует canBeSet из предыдущего effect с другим значением и очистится эта переменная только по завершении запроса.

Запросы при изменении параметра с предотвращением лишних обновлений с использованием AbortController

Лучшим решением, на мой взгляд, является использование AbortController. Это специальное api для предотвращения запроса. Подробнее можно прочитать тут.

В двух словах: в стоке 82 создается контроллер, в строке 85, в signal опцию fetch передаем controller.signal, так мы связываем контроллер с запросом. И в строке 93 в cleanup прерываем запрос, ведь cleanup вызовется при изменении id, значит нам уже нужны другие данные. Ну и в строках 88-90 проверяем, был ли запрос предотвращен, и работаем с состоянием только, если запрос не предотвращен.

Запрос при нажатии на кнопку и при обновлении id

Бывают ситуации, когда функцию запроса мы используем не только в useEffect, но и, например, при нажатии на кнопку. В этом случае нужно использовать такую конструкцию.

В этом конкретном случае функцию doRequest можно было бы вынести из компонента, но бывают случаи, когда функция опирается на какую-то переменную в рамках компонента и вытащить ее не получится. Для демонстрации подобной зависимости создана переменная type (81 строка).

Функция doRequest принимает id, по которому запрашивает информацию. Она используется при монтировании компонента и обновлении id, а также при клике на кнопку.

Чтобы отправлять запрос при монтировании, нужно использовать doRequest в useEffect, что мы и делаем в строке 92. eslint попросит добавить doRequest в массив зависимостей, а также id. В результате useEffect можно прочитать следующим образом: отправь запрос при изменении id и при изменении функции запроса.

Технически - это работает, но логически - не звучит. Запрос должен отправляться только при изменении id, а не функции запроса. И есть способ этого достичь.

Запрос при нажатии на кнопку и при обновлении id (без лишних зависимостей)

Чтобы запрос отправлялся только при изменении id - достаточно исключить использование useCallback для doRequest.

Часто возникает возражение, что этот вариант хуже, ввиду того, что функция doRequest не мемоизируется, то есть создается каждый раз, при обновлении компонента. Но стоит помнить о том, что используем мы useCallback или нет, новая функция создается всегда и вот так это выглядит в useCallback.

Как ведет себя замыкание в хуках

Теперь, давайте поговорим, почему же eslint перестал требовать добавить doRequest в зависимости useEffect. Чтобы это понять, надо понимать: при обновлении компонента все переменные в нем создаются по-новой. При этом, в памяти хранится предыдущий effect (которую передавали в useEffect). Предыдущий effect завязан на предыдущее окружение, она через замыкания имеет доступ ко всем переменным, внутри компонента и значение этих переменных соответствует предыдущему состоянию компонента. Таким образом в памяти хранится два экземпляра всех переменных и функция, которую мы создаем в useMemo, useCallback, useEffect и прочих, на самом деле не теряет замыкания, это мы "теряем" текущую функцию. Давайте разберем построчно предыдущий пример.

В useEffect при обновлении компонента, создается новая функция, и также создается новая функция doRequest. Функция effect через замыкание имеет доступ к doRequest и до тех пор, пока doRequest опирается только на константы и только на примитивы, можно быть уверенным, что от обновления, к обновлению, doRequest будет работать корректно.

Используем переменную, а не константу

Здесь мы type сделали не константой, а переменной. И теперь мы не знаем, в какой момент и где она может быть изменена. Поэтому появится предупреждение, угадайте о чем? Нужно использовать useCallback для doRequest! А в массиве зависимостей useCallback нужно добавить type.

Используем ссылочные типы

В этом примере используем константу, но, так как мы используем не примитив, а ссылочный тип (ссылки/объекты), при обновлении компонента все функции будут замыкаться на разные массивы, несмотря на то, что выглядят одинаково.

Можно использовать useRef как стабильную переменную

useRef сохраняет свое значение, вне окружения компонента, поэтому его значение от обновления к обновлению сохраняется. Несмотря на то, что в строке 115 по-прежнему создается новый массив, useRef возвращает тот массив, что был передан при монтировании компонента.

Вернемся к нашему примеру выше. По сути, этот пример

Равен вот этому

За исключением того, что doRequest в первом примере можно использовать вне useEffect.

Контроль запросов в useEffect с помощью useRef

Не всегда нужно, чтобы срабатывал effect при каждом изменении массива зависимостей. Часто нужно как-то проконтролировать его поведение, при этом не добавляя в массив зависимостей лишних переменных. В этом поможет useRef с его способностями быть стабильной переменной. Примеры из этого блока можно посмотреть тут.

Вот пример того, как выполнить первый запрос, но предотвратить все последующие

В строке 8 создаем переменную wasQueried и записываем в нее значение false, дословно это значит: "был запрошен? - нет". В строке 10, проверяем, если запрос еще не был сделан, то отмечаем, что теперь был, и отправляем запрос. Теперь запрос будет отправлен только один раз, все остальные изменения id не приведут к повторным запросам.

Этот пример, больше академический, показывает, как можно использовать useRef в useEffect. В нашем, конкретном случае будет оптимальнее использовать этот вариант:

Мы копируем id с помощью useRef и теперь idCopy - это стабильная переменная и useEffect не требует указать ее в массиве зависимостей. И useEffect выполняется все 1 раз, так как массив зависимостей пустой.

Заключение

В этой статье разобрали главное: при обновлении компонента его тело выполняется повторно, и все переменные/функции создаются повторно. Поэтому функции, что были переданы в хуки useEffect, useMemo, useCallback замыкаются на переменные из предыдущего состояния компонента. Это важное понимание позволяет понять, как писать код чище и без лишних зависимостей. А также становится понятно, почему useRef не требуется указывать в массивах зависимостей, этот хук возвращает одну и ту же переменную из памяти, этому при любом обновлении компонента, можно опираться на него.

В заключение приглашаю всех на бесплатный урок, в рамках которого напишем фронтенд небольшой админ-панели с применением Refine и Tailwind.

Теги:
Хабы:
+8
Комментарии 5
Комментарии Комментарии 5

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS