Как упростить двустороннее связывание в 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.

    Часто задаваемые вопросы