// Плохой пример: избыточные зависимости и отсутствие очистки
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection(roomId);
connection.on('message', (msg) => {
setMessages([...messages, msg]); // Ошибка: устаревший замыкание
});
connection.connect();
}, [roomId]); // Пропущена зависимость messages
// ...
}
// Улучшенная версия
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection(roomId);
let isMounted = true;
const handleMessage = (msg) => {
if (isMounted) setMessages(prev => [...prev, msg]); // Функциональное обновление
};
connection.on('message', handleMessage);
connection.connect();
return () => {
isMounted = false;
connection.off('message', handleMessage);
connection.disconnect();
};
}, [roomId]); // Правильные зависимости
// ...
}
Когда useEffect становится проблемой
React Hooks кардинально изменили разработку, но useEffect
часто становится источником трудноуловимых багов. Основная проблема — неявные зависимости и неправильная синхронизация побочных эффектов с состоянием компонента.
Типичные симптомы проблем с useEffect:
- Бесконечные циклы рендеринга
- Утечки памяти при размонтировании компонентов
- "Дребезг" выполнения эффектов
- Использование устаревших значений через замыкания
- Коллизии асинхронных операций (race conditions)
React не зря предупреждает о пропущенных зависимостях — зависимости работают как декларативная связь между эффектом и состоянием компонента.
Рецепт для безопасного useEffect
1. Точно определяем зависимости
Каждое значение внутри эффекта, которое может измениться между рендерами, должно быть в массиве зависимостей. Но это не значит добавлять всё подряд:
// Избыточная зависимость
useEffect(() => {
const filtered = largeArray.filter(item => item.active);
setVisibleItems(filtered);
}, [largeArray]); // При каждом изменении largeArray происходит выполнение
// Оптимизированный вариант
useEffect(() => {
const filtered = largeArray.filter(item => item.active);
setVisibleItems(filtered);
}, [largeArray.length]); // Зависим только от изменения длины
Для объектов используйте мемоизацию через useMemo
чтобы избежать ненужных срабатываний.
2. Управляем циклом жизни асинхронных операций
Самые опасные баги возникают, когда компонент обновляется или размонтируется во время выполнения асинхронной операции:
useEffect(() => {
let isActive = true;
const fetchData = async () => {
const result = await api.fetch(roomId);
if (isActive) setData(result); // Прервать если компонент размонтирован
};
fetchData();
return () => {
isActive = false; // Флаг отмены при размонтировании
};
}, [roomId]);
3. Используем функции очистки правильно
Очистка нужна не только для отмены запросов:
useEffect(() => {
const keydownHandler = (e) => {
if (e.key === 'Escape') handleClose();
};
document.addEventListener('keydown', keydownHandler);
return () => {
document.removeEventListener('keydown', keydownHandler);
};
}, [handleClose]); // Зависимость от стабильной функции
Важно: функция очистки запускается не только при размонтировании, но и перед повторным выполнением эффекта.
Альтернативы useEffect
Не все побочные эффекты должны находиться в useEffect. Иногда лучше использовать другие подходы:
Обработка событий — для пользовательских действий подходит лучше useEffect:
// Проблемный вариант useEffect
useEffect(() => {
const handleScroll = () => { /* ... */ };
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Решение через событие
<div onScroll={handleScroll}>...</div>
Запросы данных при Route-изменениях — используйте фреймворк-специфичные подходы или React Router loaders.
Реакция на пропсы — часто можно решить подъёмом состояния или передачей колбэка.
Производительность: как избежать лишних выполнений
Синхронизация побочных эффектов должна быть точной. Следуйте этим правилам:
- Разделяй и властвуй: Разбивайте большие useEffect на специализированные:
// Вместо одного сложного эффекта
useEffect(() => {
// Загрузка данных
// Подписка на события
// Обработка таймера
}, [dep1, dep2, dep3]);
// Специализированные эффекты
useEffect(() => { /* Загрузка */ }, [dep1]);
useEffect(() => { /* Подписка */ }, [dep2]);
useEffect(() => { /* Таймер */ }, [dep3]);
- Обходим взрывающиеся объекты зависимостей:
// Проблема: объект конфига меняется при каждом рендере
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal });
return () => controller.abort();
}, [config]); // config ссылочно меняется на каждый рендер
// Решение: примитивные зависимости
useEffect(() => {
const controller = new AbortController();
fetch(`${BASE_URL}?page=${config.page}&limit=${config.limit}`, {
signal: controller.signal
});
return () => controller.abort();
}, [config.page, config.limit]); // Изменяем только при смене страницы
- Предотвращение избыточного рендеринга с помощью
useRef
:
function Autosave({ data }) {
const lastSavedRef = useRef(0);
useEffect(() => {
const now = Date.now();
if (now - lastSavedRef.current > 60000) {
saveToServer(data);
lastSavedRef.current = now;
}
}, [data]);
// ...
}
Когда стоит пересмотреть архитектуру
Если ваш компонент содержит сложную цепочку useEffect из 5+ блоков с взаимозависимостями, проблема не в коде, а в модели состоянии. Рассмотрите:
- Переход на состояние машины (state machines) через библиотеку XState
- Логика в пользовательских хуках:
// Сложный компонент
function OrderProcess() {
const [step, setStep] = useState('cart');
const [items, setItems] = useState([]);
const [payment, setPayment] = useState(null);
useEffect(() => { /* ... */ }, [step, items]);
useEffect(() => { /* ... */ }, [payment, step]);
// ...
}
// Вынесенная логика
const {
step, items, payment,
addItem, removeItem, selectPayment
} = useOrderProcessState();
- React Context + useReducer для сложного разделяемого состояния.
Проверочный лист перед использованием useEffect
- Декларативность: Можно ли выразить это как реакцию на событие? Если да — возможно, useEffect здесь лишний.
- Зависимости: Все зависимости действительно нужны? Совпадают ли они с внутренними значениями?
- Очистка: Есть ли ресурсы, требующие освобождения (таймеры, подписки, соединения)?
- Асинхронность: Есть ли race conditions? Отменыются ли неактуальные запросы?
- Производительность: Не выполняется ли эффект чаще необходимого? Нужна ли мемоизация?
На практике опытные React-разработчики постепенно эволюционируют в понимании эффектов — от "способа выполнения кода после рендера" к мысленной модели синхронизации с внешней системой. Когда ваш эффект документирует как состояние компонента взаимодействует с API, DOM или сетевым соединением, вы начинаете видеть компоненты как систему контролируемых синхронизаций.
Элегантное использование useEffect
— не количество кода, а его точность и предсказуемость. Когда каждый эффект соответствует принципу "подписаться при появлении, отписаться при исчезновении, обновить при изменении зависимости", ошибки синхронизации становятся исключением, а не правилом.
Не существует ситуаций, где стоит игнорировать предложения линтера по зависимостям — любое исключение требует явного объяснения в комментарии. Например, когда вы контролируете монтирование компонента через флаг, как в примере с isActive
.
В React будущего с параллельными функциями (Suspense, Concurrent Mode) модель работы с эффектами будет меняться: уже сейчас экспериментируйте с useSyncExternalStore
для интеграции с внешними хранилищами и startTransition
для критичных к рендерингу обновлений.