В каждом третьем React-приложении, где я проводил код-ревью, обнаруживалась одна и та же проблема: некорректное использование хука useEffect
. Разработчики добавляют эффекты для подписок, анимаций или выборки данных, но забывают про две ключевые вещи: управление зависимостями и очистку ресурсов. Последствия — утечки памяти, неконтролируемые ререндеры и глючный UI.
Проблема: эффекты-зомби
Рассмотрим типичный пример с подпиской на внешний источник данных:
function StockTicker({ symbol }) {
const [price, setPrice] = useState();
useEffect(() => {
const ws = new WebSocket('wss://api.stocks.com');
ws.onmessage = (msg) => {
setPrice(JSON.parse(msg.data).price);
};
}, [symbol]);
return <div>{price ?? 'Loading...'}</div>;
}
Здесь сразу три ошибки:
- Отсутствие очистки сокета при размонтировании
- Неправильный массив зависимостей
- Потенциальная утечка памяти при быстрой смене пропсов
Исправленная версия:
function StockTicker({ symbol }) {
const [price, setPrice] = useState();
useEffect(() => {
let isMounted = true;
const ws = new WebSocket(`wss://api.stocks.com/${symbol}`);
const handleMessage = (msg) => {
if (!isMounted) return;
setPrice(JSON.parse(msg.data).price);
};
ws.onmessage = handleMessage;
return () => {
isMounted = false;
ws.close();
};
}, [symbol]);
return <div>{price ?? 'Loading...'}</div>;
}
Ключевые изменения:
- Флаг
isMounted
предотвращает обновление состояния размонтированного компонента - Явное закрытие WebSocket в функции очистки
- Динамическое построение URL на основе
symbol
в теле эффекта
Как useEffect работает на самом деле
Механизм эффектов в React часто становится источником недопонимания. Рассмотрим его жизненный цикл:
-
Монтирование
→ Выполняется тело эффекта
→ React сохраняет возвращённую функцию очистки -
Обновление
→ Выполняется функция очистки предыдущего эффекта
→ Выполняется тело нового эффекта
→ Сохраняется новая функция очистки -
Размонтирование
→ Выполняется последняя функция очистки
Это значит, что при каждом изменении зависимостей предыдущий эффект полностью уничтожается. Непонимание этого приводит к ошибкам при работе с асинхронными операциями.
Асинхронные эффекты: тонкости обработки
Попытка использовать async/await напрямую в useEffect — классическая ошибка:
// Так делать нельзя!
useEffect(async () => {
const data = await fetchData();
setState(data);
}, []);
React не может корректно обработать асинхронную функцию очистки. Правильный подход:
useEffect(() => {
let isActive = true;
const loadData = async () => {
const data = await fetchData();
if (isActive) {
setState(data);
}
};
loadData();
return () => {
isActive = false;
};
}, []);
Особое внимание здесь на:
- Флаг
isActive
для отмены устаревших запросов - Отдельная асинхронная функция внутри эффекта
- Возможность расширения для отмены fetch через AbortController
Оптимизация производительности
Избыточные ререндеры из-за эффектов — частая причина медленной работы приложений. Решение — тщательный подбор зависимостей:
const [user, setUser] = useState(null);
const [projects, setProjects] = useState([]);
// Плохо: эффект срабатывает при любом изменении user
useEffect(() => {
if (user?.id) {
fetchProjects(user.id).then(setProjects);
}
}, [user]);
// Хорошо: выделяем конкретную зависимость
useEffect(() => {
if (user?.id) {
fetchProjects(user.id).then(setProjects);
}
}, [user?.id]); // Сработает только при изменении ID пользователя
Но тут кроется новый подводный камень: если user
обновляется как новый объект с тем же ID, эффект выполнится снова. Для сложных объектов используйте хеширование зависимостей:
const userHash = JSON.stringify({ id: user.id, role: user.role });
useEffect(() => {
// Логика эффекта
}, [userHash]);
Когда не стоит использовать useEffect
30% случаев использования эффектов — попытки синхронизировать состояние, что лучше решается через:
-
Вычисляемые значения
javascriptconst fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName] );
-
Обработчики событий
Для действий по клику/вводу используйте колбеки, а не эффекты -
Сброс состояния при пропс-изменениях
Вместо эффектов сsetState
используйте:javascriptfunction ProfilePage({ userId }) { const [comment, setComment] = useState(''); // Это сбрасывает состояние при смене userId return <Profile userId={userId} key={userId} />; }
Инструменты отладки
Для сложных эффектов используйте:
useEffectEvent
(экспериментальный хук в React 19)- Кастомные хуки-обёртки с логированием
- React DevTools Profiler для отслеживания ненужных выполнений
Пример хука с отладкой:
function useDebugEffect(name, effect, deps) {
useEffect(() => {
console.log(`[EFFECT START] ${name}`);
const cleanup = effect();
return () => {
console.log(`[EFFECT CLEANUP] ${name}`);
cleanup?.();
};
}, deps);
}
Эффекты — мощный, но опасный инструмент. Как говорит принцип React: «Покажите мне ваш код на useEffect, и я скажу, какой у вас стаж разработки». Используйте их осознанно, всегда проверяйте ESLint-правила для зависимостей, и помните — часто лучший эффект это тот, которого нет в коде.