5 самых распространенных ошибок при использовании useEffect в React и их решения
Автор: Javohir Abdullayev · · React и Frontend

Почему хук useEffect часто становится головной болью?
С момента перехода на функциональные компоненты в экосистеме React хук useEffect стал одним из самых используемых, но в то же время одним из самых неправильно понимаемых инструментов. Многие начинающие и даже опытные фронтенд-разработчики воспринимают его как простую замену методов жизненного цикла классовых компонентов — componentDidMount или componentDidUpdate. На самом же деле useEffect — это механизм синхронизации состояния (state) компонента с внешним миром.
Неправильно написанные эффекты приводят к бесконечным повторным рендерам (infinite re-render), утечкам памяти (memory leak), непредсказуемому поведению данных и снижению производительности приложения. В этой статье мы подробно рассмотрим 5 наиболее распространенных на практике ошибок и профессиональные способы их устранения.
Ошибка 1: Бесконечный цикл рендеринга (Infinite Re-render Loop)
Самая частая ошибка среди разработчиков: как только компонент отображается на экране, браузер зависает или в консоли появляется ошибка 'Maximum update depth exceeded'. Обычно это происходит при неправильно указанном массиве зависимостей (dependency array) или когда объект создается заново при каждом рендере.
Пример ошибочного кода:
function UserProfile({ userId }) { const [user, setUser] = useState(null); const options = { detailed: true }; useEffect(() => { fetchUser(userId, options).then(data => setUser(data)); }, [options]); }Проблема заключается в том, что в JavaScript при каждом рендере в памяти создается новый объект options. Даже если его свойства идентичны, ссылка на объект в памяти обновляется. React видит изменение в массиве зависимостей и заново запускает эффект. Вызов setUser внутри эффекта снова провоцирует рендер, замыкая бесконечный цикл.
Правильное решение:
function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { const options = { detailed: true }; fetchUser(userId, options).then(data => setUser(data)); }, [userId]); }Объявляйте объекты или массивы непосредственно внутри useEffect, а не в теле компонента, либо используйте хук useMemo, если объект передается извне.
Ошибка 2: Устаревшее замыкание (Stale Closures)
Начинающие разработчики часто игнорируют предупреждения ESLint и оставляют массив зависимостей пустым: []. В результате из-за механизма замыкания (closure) функция внутри эффекта «видит» только начальное значение переменной.
Пример ошибочного кода:
function Timer() { const [seconds, setSeconds] = useState(0); useEffect(() => { const interval = setInterval(() => { setSeconds(seconds + 1); }, 1000); return () => clearInterval(interval); }, []); return <h1>Vaqt: {seconds}</h1>; }Этот таймер никогда не превысит 1! Дело в том, что значение seconds внутри setInterval навсегда остается равным 0.
Правильное решение:
function Timer() { const [seconds, setSeconds] = useState(0); useEffect(() => { const interval = setInterval(() => { setSeconds(prev => prev + 1); }, 1000); return () => clearInterval(interval); }, []); return <h1>Vaqt: {seconds}</h1>; }Всегда используйте функциональное обновление состояния (prev => prev + 1), опирающееся на предыдущее значение. Это позволяет сохранять массив зависимостей чистым.
Ошибка 3: Отсутствие функции очистки (Cleanup)
Отсутствие этапа очистки перед размонтированием компонента (unmount) или перед повторным запуском эффекта приводит к серьезным утечкам памяти. Примерами могут служить обработчики событий (event listeners), WebSocket-соединения или таймеры.
Правильное решение:
useEffect(() => { function handleResize() { console.log(window.innerWidth); } window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); }; }, []);Подписались на внешнее событие внутри эффекта? Обязательно возьмите в привычку отписываться от него с помощью return () => { ... }.
Ошибка 4: Состояние гонки (Race Condition) при асинхронных запросах
Когда пользователь быстро вводит текст в поле поиска или переключается между профилями, на сервер отправляется несколько запросов. Из-за сетевых задержек самый первый запрос может прийти позже последнего, и на экране отобразятся некорректные данные.
Правильное решение (с AbortController):
useEffect(() => { const controller = new AbortController(); async function loadData() { try { const res = await fetch(`/api/user/${userId}`, { signal: controller.signal }); const data = await res.json(); setUser(data); } catch (err) { if (err.name !== 'AbortError') { console.error(err); } } } loadData(); return () => { controller.abort(); }; }, [userId]);С помощью AbortController при изменении параметра старый запрос немедленно отменяется, и на экране отображается только актуальный результат последнего запроса.
Ошибка 5: Использование useEffect для любых вычислений
Многие разработчики для вычисления производных данных создают отдельное состояние и обновляют его внутри useEffect. Это вызывает лишние рендеры и усложняет код.
Неправильный подход:
const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [fullName, setFullName] = useState(''); useEffect(() => { setFullName(`${firstName} ${lastName}`); }, [firstName, lastName]);Правильный подход:
const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const fullName = `${firstName} ${lastName}`;Если значение можно просто вычислить на основе существующих props или state, отдельное состояние и useEffect вообще не требуются. Если же вычисления ресурсоемкие, достаточно использовать useMemo.
Заключение
useEffect — мощный инструмент, но использовать его нужно осознанно и в меру. Каждый раз при написании эффекта задавайте себе вопросы: действительно ли здесь нужна синхронизация с внешней системой, корректно ли указаны зависимости и предусмотрена ли функция очистки? Следуя этим 5 правилам, вы повысите производительность приложения и избавитесь от неочевидных багов.
Теги: #React #React Hooks #useEffect #Frontend #JavaScript