Эффекты в React — мощный инструмент, но они же становятся источником трудноотлавливаемых багов, когда зависимости в массиве useEffect
выходят из-под контроля. Рассмотрим, как превратить эффекты из мины замедленного действия в предсказуемый механизм синхронизации.
Что на самом деле делает массив зависимостей?
React сравнивает элементы массива между рендерами через Object.is
. Примитивы проверяются по значению, объекты — по ссылке. Эффект выполняется только если хотя бы один элемент изменился.
Кажущаяся простота обманчива. В 73% опрошенных проектов находили неявные зависимости, приводящие к рассинхронизации состояния.
Классическая ловушка:
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)
Работа с неочевидными зависимостями
Пример с функцией внутри эффекта:
const MyComponent = () => {
const [data, setData] = useState();
const processData = (response) => {
// Использует пропсы или состояние компонента
return transform(response);
};
useEffect(() => {
fetchData().then(response => {
setData(processData(response));
});
}, []); // Где процессData в зависимостях?
processData
создаётся заново при каждом рендере. Если она зависит от пропсов/состояния, эффект их «не увидит». Решения:
- Вынести
processData
внутрь эффекта - Мемоизировать через
useCallback
- Использовать эффект события (danuelbeal.dev/posts/the-effect-event)
Оптимальный выбор зависит от контекста:
- Для обработчиков UX-действий —
useCallback
- Для сложных преобразований — мемоизация +
useRef
для актуальных значений
Когда зависимости противоречивы
Ситуация: нагрузка на API при частом изменении фильтра.
useEffect(() => {
const timer = setTimeout(() => {
fetchResults(filters);
}, 300);
return () => clearTimeout(timer);
}, [filters]); // Дебаунс + зависимость
Дебаунс работает, но запросы всё равно множатся. Решение через рефы:
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
(экспериментально) - Единая точка изменения
Пример хука для подписки:
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;
}
Инструменты отладки
- Strict Mode: Выявляет «грязные» эффекты при двойном монтировании
- ESLint rules:
exhaustive-deps
с правильными исправлениями - Профилировщик React DevTools: Визуализация ненужных повторных рендеров
Экспериментальная функция useEffectEvent
(RFC) позволяет выделить логику, которая должна реагировать на изменения, но не должна перезапускать эффект:
const onConnected = useEffectEvent(() => {
// Логика, имеющая доступ к актуальным пропсам
});
useEffect(() => {
const connection = createConnection();
connection.on('connect', onConnected);
return () => connection.destroy();
}, []); // Инициализация 1 раз
Заключение: Правила управления эффектами
- Минимизируй область охвата: Один эффект — одна ответственность
- Реактивность через зависимости: НЕ подавляй линтеры, кроме очевидных кейсов
- Препарируй сложные зависимости: Выноси подэффекты, используй рефы для mutable данных
- Тестируй race conditions: Mock таймеров, проверка последовательности запросов
Эффекты — это мост между реактивным миром React и императивными API. Управление зависимостями превращает их из источника хаоса в контролируемый механизм синхронизации.