5 самых распространенных ошибок при использовании useEffect в React и их решения

Автор: Javohir Abdullayev · · React и Frontend

5 самых распространенных ошибок при использовании useEffect в React и их решения

Почему хук 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