Как упростить двустороннее связывание в React и избежать лишних срабатываний useEffect
Разработчики, переходящие с Vue на React, часто сталкиваются с двумя проблемами: громоздкая запись двустороннего связывания (аналог v-model) и нежелательные срабатывания useEffect при изменении ссылки на объект (аналог watch). Рассмотрим практические решения на примере фильтра с выбором месяца.
Проблема 1: как упростить двустороннее связывание?
В React нет встроенной директивы v-model, как во Vue. Каждый onChange требует ручного обновления состояния через spread-оператор. Это приводит к многострочным конструкциям:
setFilters(prev => ({
...prev,
filters: {...prev.filters, month: val}
}))Решение: кастомный хук useForm или useObjectState
Создайте универсальный хук, который принимает путь к полю и автоматически обновляет вложенные объекты:
function useObjectState(initial) {
const [state, setState] = useState(initial);
const setField = (path, value) => {
setState(prev => {
const keys = path.split('.');
const lastKey = keys.pop();
const newObj = {...prev};
let current = newObj;
keys.forEach(key => { current[key] = {...current[key]}; current = current[key]; });
current[lastKey] = value;
return newObj;
});
};
return [state, setField];
}Использование:
const [filters, setField] = useObjectState({
page: 1,
perPage: 20,
filters: { month: { month: 3, year: 2025 } }
});
// в компоненте:
<MonthPicker
value={filters.filters.month}
onChange={(val) => setField('filters.month', val)}
/>Также можно использовать библиотеки formik, react-hook-form или use-immer для работы с иммутабельными данными.
Проблема 2: useEffect срабатывает при том же значении, но новой ссылке
В React useEffect сравнивает зависимости по ссылке (===). Если вы передаёте новый объект (даже с теми же данными), эффект выполняется заново. Пример:
useEffect(() => { loadData(); }, [filters.filters.month]);
// сработает, если month изменился по ссылке, а не по значениюРешение: глубокое сравнение зависимостей
Используйте useMemo для стабилизации ссылки на объект, если данные не изменились:
const stableMonth = useMemo(() => filters.filters.month, [
filters.filters.month.month,
filters.filters.month.year
]);
useEffect(() => { loadData(); }, [stableMonth]);Или воспользуйтесь пользовательским хуком useDeepCompareEffect из библиотеки use-deep-compare-effect:
import useDeepCompareEffect from 'use-deep-compare-effect';
useDeepCompareEffect(() => { loadData(); }, [filters.filters.month]);Третий вариант - сериализовать объект в строку (JSON.stringify) и сравнивать строки, но это менее производительно для частых изменений.
Сравнение с Vue: v-model и watch
Во Vue двустороннее связывание реализуется через v-model (одна строка), а слежение за глубокими изменениями - через watch с опцией deep: true. React предлагает больше контроля, но требует дополнительных усилий. Выбор подхода зависит от проекта: для простых форм - кастомные хуки, для сложных - библиотеки.
Заключение
Упростить двустороннее связывание в React можно с помощью кастомных хуков или готовых решений (formik, react-hook-form). Для предотвращения лишних срабатываний useEffect используйте стабилизацию ссылок через useMemo или библиотеки глубокого сравнения. Эти техники помогут писать более чистый и предсказуемый код, приближаясь к удобству Vue.