Избегаем хаоса: Стратегии работы с зависимостями useEffect в React

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

Что на самом деле делает массив зависимостей?

React сравнивает элементы массива между рендерами через Object.is. Примитивы проверяются по значению, объекты — по ссылке. Эффект выполняется только если хотя бы один элемент изменился.

Кажущаяся простота обманчива. В 73% опрошенных проектов находили неявные зависимости, приводящие к рассинхронизации состояния.

Классическая ловушка:

javascript
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(response => response.json())
      .then(setUser);
  }, []); // Пропущена зависимость userId
}

При смене userId эффект не перезапускается. Исправление кажется очевидным — добавить [userId], но на практике:

  • При частых изменениях userId — множество запросов
  • Последовательность ответов может не совпадать с порядком запросов (race condition)

Работа с неочевидными зависимостями

Пример с функцией внутри эффекта:

javascript
const MyComponent = () => {
  const [data, setData] = useState();
  const processData = (response) => {
    // Использует пропсы или состояние компонента
    return transform(response);
  };

  useEffect(() => {
    fetchData().then(response => {
      setData(processData(response));
    });
  }, []); // Где процессData в зависимостях?

processData создаётся заново при каждом рендере. Если она зависит от пропсов/состояния, эффект их «не увидит». Решения:

  1. Вынести processData внутрь эффекта
  2. Мемоизировать через useCallback
  3. Использовать эффект события (danuelbeal.dev/posts/the-effect-event)

Оптимальный выбор зависит от контекста:

  • Для обработчиков UX-действий — useCallback
  • Для сложных преобразований — мемоизация + useRef для актуальных значений

Когда зависимости противоречивы

Ситуация: нагрузка на API при частом изменении фильтра.

javascript
useEffect(() => {
  const timer = setTimeout(() => {
    fetchResults(filters);
  }, 300);
  return () => clearTimeout(timer);
}, [filters]); // Дебаунс + зависимость

Дебаунс работает, но запросы всё равно множатся. Решение через рефы:

javascript
const latestRequestId = useRef(0);

useEffect(() => {
  const currentId = ++latestRequestId.current;
  
  const timer = setTimeout(async () => {
    const result = await fetchResults(filters);
    
    if (currentId === latestRequestId.current) {
      setData(result);
    }
  }, 300);
  
  return () => clearTimeout(timer);
}, [filters]);

Этот паттерн:

  • Отменяет устаревшие запросы
  • Сохраняет порядок ответов
  • Не требует внешних библиотек

Извлекаем сложную логику в кастомные хуки

Дублирование эффектов — красный флаг. Инкапсуляция в хуки решает:

  • Явные зависимости через параметры
  • Использование useEffectEvent (экспериментально)
  • Единая точка изменения

Пример хука для подписки:

javascript
function useWebSocket(url, onMessage) {
  const ws = useRef();

  useEffect(() => {
    ws.current = new WebSocket(url);
    ws.current.onmessage = (event) => {
      onMessage(event.data);
    };
    
    return () => ws.current.close();
  }, [url]); //  Явная зависимость URL

  const send = useCallback((message) => {
    ws.current.send(message);
  }, []);

  return send;
}

Инструменты отладки

  1. Strict Mode: Выявляет «грязные» эффекты при двойном монтировании
  2. ESLint rules: exhaustive-deps с правильными исправлениями
  3. Профилировщик React DevTools: Визуализация ненужных повторных рендеров

Экспериментальная функция useEffectEvent (RFC) позволяет выделить логику, которая должна реагировать на изменения, но не должна перезапускать эффект:

javascript
const onConnected = useEffectEvent(() => {
  // Логика, имеющая доступ к актуальным пропсам
});

useEffect(() => {
  const connection = createConnection();
  connection.on('connect', onConnected);
  return () => connection.destroy();
}, []); // Инициализация 1 раз

Заключение: Правила управления эффектами

  1. Минимизируй область охвата: Один эффект — одна ответственность
  2. Реактивность через зависимости: НЕ подавляй линтеры, кроме очевидных кейсов
  3. Препарируй сложные зависимости: Выноси подэффекты, используй рефы для mutable данных
  4. Тестируй race conditions: Mock таймеров, проверка последовательности запросов

Эффекты — это мост между реактивным миром React и императивными API. Управление зависимостями превращает их из источника хаоса в контролируемый механизм синхронизации.

text