useWatch и useEffect в React Hook Form: как избежать лишних срабатываний
При разработке сложных форм на React с библиотекой React Hook Form (RHF) разработчики часто сталкиваются с проблемой, когда useEffect срабатывает чаще, чем ожидалось. Это происходит из-за особенностей работы хука useWatch и механизма сравнения зависимостей в React. В этой статье мы подробно разберём причины такого поведения, предложим оптимальные решения и ответим на частые вопросы.
Почему useEffect срабатывает лишний раз?
Когда вы используете useWatch для отслеживания нескольких полей формы, он возвращает массив значений. При каждом изменении любого из отслеживаемых полей создаётся новый массив, даже если фактические значения остались прежними. React сравнивает зависимости по ссылке (reference equality), а не по содержимому. Поэтому useEffect, в массиве зависимостей которого есть элементы из этого массива, будет считать их изменившимися и выполнит колбэк.
В исходном примере facilities - это объект или массив, который при каждом рендере получает новую ссылку. Даже если данные не изменились логически, useEffect срабатывает, так как зависимость facilities теперь указывает на другой объект в памяти.
Правильный способ: разделение useWatch на отдельные вызовы
Наиболее надёжное решение - вызывать useWatch отдельно для каждого поля, которое должно быть в зависимостях useEffect. Это гарантирует, что эффект сработает только при изменении конкретного значения, а не всего массива.
const isRevolving = useWatch({ control, name: 'is_revolving' });
const ratesType = useWatch({ control, name: 'rates_and_liquidity_type' });
const facilities = useWatch({ control, name: 'facilities' });
useEffect(() => {
void trigger('payerGroupRates');
}, [isRevolving, ratesType, facilities, trigger]);Этот подход решает проблему лишних срабатываний, так как каждый вызов useWatch возвращает примитивное значение (строку, число) или один и тот же объект, если данные не изменились. Разделение useWatch - хорошая практика, особенно когда количество отслеживаемых полей невелико.
Альтернативные подходы
Глубокая проверка зависимостей
Если вы не хотите разделять useWatch, можно использовать библиотеки для глубокого сравнения, например lodash.isequal, и реализовать собственную логику в useEffect:
import isEqual from 'lodash.isequal';
const prevDeps = useRef();
useEffect(() => {
const currentDeps = [isRevolving, facilityGroup, facilities];
if (!isEqual(prevDeps.current, currentDeps)) {
void trigger('payerGroupRates');
}
prevDeps.current = currentDeps;
}, [isRevolving, facilityGroup, facilities, trigger]);Этот метод даёт точную реактивность, но добавляет сложность и может снизить производительность при большом объёме данных.
Использование useMemo для стабилизации ссылок
Другой вариант - обернуть возвращаемый массив useWatch в useMemo с явным указанием зависимостей. Однако это всё равно требует ручного перечисления полей и не всегда удобно.
Когда стоит разделять useWatch?
- Количество полей невелико (до 5-6). Разделение улучшает читаемость и производительность.
- Поля используются в разных эффектах или компонентах. Это уменьшает количество ненужных перерендеров.
- Вы хотите избежать неочевидных багов, связанных с ссылочной идентичностью.
Когда лучше оставить один вызов?
- Если все значения используются вместе в одном месте и не вызывают проблем с эффектами.
- Если поля - примитивы (строки, числа) и не приводят к лишним срабатываниям.
- Если вы используете
useWatchтолько для отображения данных, а не для триггеров.
Выводы
Проблема лишних срабатываний useEffect при использовании useWatch возникает из-за создания нового массива при каждом изменении. Лучшая практика - разделять useWatch на отдельные вызовы для каждого поля, которое является зависимостью эффекта. Это делает код предсказуемым, улучшает производительность и упрощает отладку. Альтернативные методы (глубокое сравнение, useMemo) тоже работают, но требуют дополнительных усилий и могут быть избыточными.