Заслуживает ли этот хук своего места? Десять вопросов для обзора кода на React
Узнайте десять конкретных вопросов для проверки, которые помогают выявить избыточное состояние, устаревшие эффекты, циклы рендеринга, ненужную мемоизацию и слишком крупные компоненты в коде на React Hook.
Источники: Hooks
Большинство ошибок React Hook не выглядят как ошибки в пул-реквесте. Отсутствующая зависимость, эффект, копирующий параметры в состояние, или использование useCallback вокруг обработчика, который никто не мемоизирует, проходят проверку, а затем проявляются позже в виде устаревших экранов, лишних отрисовок или накопившихся слушателей. Ниже приведены десять распространенных ошибок Hook, превращенные в вопросы для проверки любого компонента, с объяснением механизма каждой из них и более простым вариантом кода, который обычно требуется.
Вопросы о состоянии
Можно ли вычислить это значение во время отрисовки?
Распространённый подход заключается в хранении значения fullName в состоянии компонента и использовании эффекта для вызова setFullName(`${firstName} ${lastName}`) каждый раз, когда меняется хотя бы одно из имён. Компонент отрисовывается один раз с старым значением, затем выполняется эффект после отрисовки, и функция-установщик запускает вторую отрисовку. В течение одного кадра интерфейс остаётся несогласованным, к тому же требуется дополнительная логика для его поддержания.
Лучше вычислять значение напрямую: const fullName = `${firstName} ${lastName}`;. React перезапускает компонент при каждой отрисовке, поэтому значение всегда синхронизировано с входными данными. Если что-то можно получить из пропсов или состояния, это не считается состоянием. См. также почему синхронизация состояния с useEffect имеет обратный эффект.
Содержит ли компонент больше состояния, чем ему нужно?
Та же ошибка в более крупном масштабе: форма с тремя вызовами useState для firstName, lastName и fullName. Теперь любое изменение имени должно отражаться в fullName, и любая ошибка в этом приводит к багу. Лучше сохранить два независимых поля ввода и формировать третье на основе них. Для каждого useState проверьте, можно ли воссоздать значение на основе остальных; если да, удаляйте этот useState.
Требуется ли обновление экрана при изменении этого значения?
Состояние существует для управления отрисовкой. Идентификатор таймера, узел DOM, предыдущее значение свойства или инстанция стороннего приложения (карта, график, клиент сокета) обычно не влияют на то, что отрисовывается, поэтому использование useState(null) в таких случаях приводит к бесполезной отрисовке при каждом присваивании. Вместо этого используйте const timerRef = useRef(null);: запись timerRef.current сохраняется между отрисовками без необходимости запуска новой отрисовки. С другой стороны, никогда не следует читать значение ref для определения того, что отображать. Если интерфейс должен отражать определённое значение, это всё равно является состоянием.
Вопросы о эффектах
Содержит ли массив зависимостей всё, что считывает эффект?
Предоставление параметра [], чтобы эффект выполнялся «один раз», как в примере useEffect(() => { fetchUser(userId); }, []);, фиксирует значение userId с момента первой отрисовки. Когда свойство меняется, ничего не обновляется, и интерфейс отображает неверного пользователя. Использование [userId] решает эту проблему.
Массив не является регулятором частоты выполнения; он указывает, какие значения будет считать эффект. Если эффект выполняется слишком часто, следует изменить данные, которые он считает (переместить функцию внутрь него, использовать примитив вместо объекта), а не скрывать зависимости. Правило проверки react-hooks/exhaustive-deps выявляет почти все такие случаи, поэтому следует рассматривать его предупреждения как ошибки. Эффекты, выполняющие запросы, также должны игнорировать или прерывать устаревшие запросы в процессе очистки, когда userId быстро меняется.
Может ли этот эффект запустить себя сам?
useEffect(() => { setCount(count + 1); }, [count]); представляет собой цикл: обновление изменяет значение count, что приводит к повторному запуску этого эффекта бесконечно. Функциональный метод обновления setCount(prev => prev + 1) считывает последнее значение без необходимости использования count в замыкании, поэтому зависимость может быть удалена, и эффект будет запускаться только при первой загрузке компонента. Обратите внимание, что он всё равно увеличивается на один единицу при каждой загрузке, а в режиме строгой проверки компонент загружается дважды во время разработки, поэтому локально значение может увеличиваться на два.
Часто более важным вопросом является то, действительно ли данное обновление должно находиться внутри эффекта. Реакция на действия пользователя происходит в обработчиках событий; производные значения вычисляются во время отрисовки. Эффекты предназначены для синхронизации с системами, находящимися вне React.
Аннулирует ли эффект то, что он настроил?
Эффект, который вызывает только window.addEventListener('resize', handleResize), добавляет нового слушателя при каждом монтировании и никогда не удаляет его. Необходимо вернуть функцию для очистки, которая вызывает window.removeEventListener с тем же обработчиком. React выполняет её перед повторным запуском эффекта и при демонтировании. Без неё возникают утечки памяти, многократное выполнение обработчиков и обновления компонентов, которых уже нет.
Краткая проверка: каждый метод add, connect или setInterval в эффекте требует соответствующего метода remove, disconnect или clearInterval в его значении возвращения. В статье аудит useEffect: десять распространённых ошибок, их симптомы и реальные решения рассматриваются дополнительные способы сбоев эффектов.
Вопросы о мемоизации
Действительно ли эти вычисления затратны?
Оборачивание операции объединения строк в useMemo с [user] в качестве зависимости стоит дороже, чем приносит пользы. React всё равно выделяет ресурсы под функцию-обратный вызов и массив, а также сравнивает зависимости при каждой отрисовке, что требует больше усилий, чем простое объединение двух строк. Код становится менее читаемым, и даже если user — новый объект при каждой отрисовке, мемоизация всё равно будет пересчитываться заново.
Используйте простое выражение. Обращайтесь к useMemo, когда речь идёт о действительно затратных операциях, таких как сортировка или фильтрация большого списка, или когда мемоизированному подкомпоненту или другому хуку требуется стабильный объект-ссылка. Сначала измерьте нагрузку с помощью профилятора React DevTools.
Зависит ли что-либо от идентичности этой функции?
useCallback обеспечивает стабильность ссылки на функцию, что имеет значение только тогда, когда происходит сравнение ссылок: например, у дочернего элемента, обёрнутого в React.memo, или в списке зависимостей другого хука. Обёртывание обработчика клика, который просто выводит лог, привязанного к обычной button, не приносит никакой пользы, поскольку DOM не интересуется тем, новая ли функция. Напишите обычную функцию и добавьте useCallback, когда сможете указать компонент, который её нуждается. Компилятор React может автоматически применять такую мемоизацию; проверьте актуальную документацию для узнания её статуса в вашей конфигурации.
Вопросы о структуре
Вызываются ли все хуки безусловно и в том же порядке?
Использование useState только внутри if (isLoggedIn) нарушает Правила хуков. React сопоставляет хуки по их местоположению в списке элементов компонента, а не по имени. Если пропустить один вызов, все последующие хуки будут использовать место соседнего хука, что приведёт к неверным значениям или ошибке во время выполнения из-за изменения количества хуков.
Вызывайте все хуки на верхнем уровне, а затем разветвляйтесь: сначала объявите состояние user, а если пользователь не вошёл, верните null. То же самое касается циклов и операций раннего возврата. Подробнее об интернутрах см. почему существуют Правила хуков.
Не выполняет ли этот компонент слишком много задач?
Компонент, в котором сотни строк кода для загрузки данных, управления состоянием и реализации бизнес-логики смешаны с маркапом, труден для чтения, тестирования и изменения. Вынесите эту логику в пользовательский хук. Хук useUserData(userId) управляет состоянием user и эффектом, который вызывает fetchUser(userId).then(setUser) при изменении userId, а затем возвращает user. Таким образом, код компонента UserProfile сводится к строке const user = useUserData(userId); и отображению значения user?.name.
Это дает вам:
- Возможность повторного использования логики без копирования её между компонентами.
- Тесты логики, независимые от маркапа.
- Более короткие компоненты, которые легко просматривать.
- Одно место для изменения процедур загрузки или кэширования данных.
Помните, что каждый вызывающий пользовательский хук получает собственное состояние: логика делится между ними, а не данные.
Основные выводы
- Получаемые значения являются выражениями, а не состоянием и не эффектами.
- Сохраняйте состояние в минимальном объёме; значения, которые никогда не отображаются на экране, помещайте в refs.
- Массивы зависимостей описывают, что читает эффект, поэтому проверяйте их тщательно.
- Каждый эффект, который что-то настраивает, должен это также деанонсировать.
- Мемоизируйте только тогда, когда это оправдано измерением или потребителем, чувствительным к ссылкам.
- Вызывайте Hooks безусловно, а крупные блоки логики переносите в пользовательские Hooks.
Большинство этих проблем возникает из-за привычки использовать Hook без раздумий. Во время ревью стоит задаться вопросом «Действительно ли нам нужен этот Hook здесь?» — это несложно и помогает избежать длительных сессий отладки.