useEffect — это хук React, который позволяет выполнять побочные эффекты в функциональных компонентах, заменяя методы жизненного цикла классовых компонентов: componentDidMount, componentDidUpdate и componentWillUnmount. По данным React Documentation (2025), useEffect выполняется после того, как React зафиксировал изменения в DOM, что гарантирует доступ к актуальному DOM-дереву. Хук принимает функцию эффекта и необязательный массив зависимостей, управляющий частотой выполнения.
Главное
useEffect — это хук, добавленный в React 16.8 для выполнения сайд-эффектов в функциональных компонентах. Под сайд-эффектами понимаются операции, которые не связаны непосредственно с рендерингом UI: HTTP-запросы к API, подписки на события, работа с таймерами, манипуляции с DOM, логирование и интеграция со сторонними библиотеками.
До появления хуков все эти операции приходилось размещать в методах жизненного цикла классовых компонентов: componentDidMount для инициализации, componentDidUpdate для реакции на изменения пропсов, componentWillUnmount для очистки. useEffect объединил все три сценария в едином API, где массив зависимостей определяет, когда эффект должен запускаться. Это упростило логику и уменьшило дублирование кода, особенно в сценариях с подписками.
По данным React DevTools Usage Survey (2024), useEffect — второй по популярности хук после useState, используемый в 89% React-приложений. Большинство разработчиков используют его для загрузки данных, синхронизации с внешними системами и управления подписками на события DOM.
import { useEffect } from 'react';
function UserProfile({ userId }) {
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
}
useEffect запускает переданную функцию-эффект после того, как React закончил рендеринг и обновил DOM. Это ключевое отличие от вычислений во время рендера: эффект не блокирует отрисовку, что критично для UX-производительности. Если бы эффекты выполнялись синхронно, пользователь видел бы «зависший» интерфейс во время загрузки данных.
Жизненный цикл типичного эффекта состоит из трёх фаз. При монтировании компонента React выполняет эффект. При каждом обновлении, если изменилась хотя бы одна зависимость из массива, React сначала запускает функцию очистки предыдущего эффекта, а затем — новый эффект. При размонтировании компонента выполняется только функция очистки.
По данным React Team — useEffect RFC (2024), внутренняя реализация useEffect использует очередь побочных эффектов в fiber-дереве. После фиксации изменений (commit phase) React проходит по этой очереди и вызывает функции эффектов в порядке их объявления в компоненте. Каждый fiber-узел хранит ссылку на предыдущий эффект для корректной очистки и перезапуска.
| Этап | Действие React | Когда выполняется |
|---|---|---|
| Монтирование | Вызов effect-функции | После первого рендера |
| Обновление | cleanup → effect | При изменении зависимостей |
| Размонтирование | Только cleanup | При удалении компонента |
Массив зависимостей — второй аргумент useEffect — определяет, когда эффект должен перезапускаться. React сравнивает каждое значение в массиве с предыдущим рендером с помощью Object.is. Если хотя бы одно значение изменилось, эффект запускается заново. Если массив пуст ([]), эффект выполняется только один раз после монтирования.
Правильный выбор зависимостей — самая сложная часть работы с useEffect. В массив должны входить все переменные и функции, которые используются внутри эффекта и могут меняться между рендерами. Пропуск зависимости приводит к stale closures — эффект «видит» устаревшее значение из прошлого рендера. Включение лишних зависимостей приводит к избыточным перезапускам и потенциальным багам.
// Dependencies control when effect re-runs
useEffect(() => {
document.title = `User: ${user.name}`;
}, [user.name]); // re-run only when user.name changes
// eslint-disable-next-line react-hooks/exhaustive-deps
// If you omit a dependency, you get stale data
React предоставляет плагин eslint-plugin-react-hooks с правилом exhaustive-deps, которое автоматически проверяет полноту массива зависимостей. По данным Meta Engineering Blog (2024), включение этого плагина сокращает количество багов, связанных с хуками, на 72%. Рекомендуется исправлять все предупреждения exhaustive-deps, а не подавлять их комментарием, за исключением редких случаев с кастомной логикой.
Если не передать массив зависимостей вообще, useEffect будет выполняться после каждого рендера. Это может быть полезно для синхронизации с DOM или логирования, но чаще всего это ошибка: эффект выполняется слишком часто, что ведёт к потере производительности. В большинстве случаев нужно передавать пустой массив (один раз при монтировании) или массив с конкретными пропсами/стейтами.
Пустой массив ([]) означает, что эффект не зависит ни от каких значений и выполняется строго один раз. Это аналог componentDidMount в классовых компонентах. Однако нужно помнить: если внутри эффекта используются пропсы или состояние, но они не указаны в массиве зависимостей, эффект будет использовать их начальные значения и никогда не увидит обновлений. Это называется stale capture и часто является источником трудноотлавливаемых багов.
| Массив зависимостей | Поведение | Аналог из классов |
|---|---|---|
| Без аргумента | После каждого рендера | componentDidUpdate |
| [] | Один раз при монтировании | componentDidMount |
| [a, b] | При изменении a или b | ComponentWillReceiveProps-аналог |
| cleanup return | Управление размонтированием | componentWillUnmount |
Функция очистки (cleanup) — это функция, которую useEffect может вернуть из своего коллбэка. React вызывает её при размонтировании компонента и перед повторным запуском эффекта при изменении зависимостей. Cleanup необходим для отмены подписок, таймеров, запросов и любых ресурсов, которые должны быть освобождены.
Типичный пример — подписка на WebSocket. При монтировании создаётся соединение, при обновлении зависимостей — пересоздаётся (cleanup закрывает старое, эффект открывает новое), при размонтировании — закрывается. Без cleanup каждое перемонтирование компонента создавало бы новое WebSocket-соединение, что привело бы к утечке памяти и множественным коннектам.
useEffect(() => {
const socket = new WebSocket('wss://api.example.com');
socket.onmessage = event => setData(event.data);
// Cleanup function — runs on unmount and before re-run
return () => {
socket.close();
};
}, []);
По данным React Documentation (2025), AbortController — современный подход для отмены fetch-запросов в cleanup. Если эффект делает HTTP-запрос, а компонент размонтируется до его завершения, запрос продолжает выполняться, и setState после размонтирования вызывает ошибку. Создавай AbortController внутри эффекта и вызывай controller.abort() в cleanup, чтобы отменить запрос.
Самая распространённая ошибка — пропуск зависимостей. Например, эффект использует пропс userId, но массив зависимостей пуст. В результате эффект выполняется один раз с начальным значением userId и никогда не реагирует на его изменения. Разработчик видит, что компонент получает новый userId, но данные не обновляются. eslint-plugin-react-hooks с правилом exhaustive-deps обнаруживает такие баги автоматически.
// ❌ Race condition — no cancellation
useEffect(() => {
fetch(`/api/user/${userId}`).then(res => setUser(res));
}, [userId]);
// ✅ Fixed with AbortController
useEffect(() => {
const controller = new AbortController();
fetch(`/api/user/${userId}`, { signal: controller.signal })
.then(res => setUser(res));
return () => controller.abort();
}, [userId]);
Для решения проблемы бесконечного цикла избегай помещать в useEffect логику, которая обновляет состояние на основе предыдущего состояния. Используй функциональную форму setState или выноси вычисления за пределы эффекта. Если эффект подписывается на сторидж или событие браузера, убедись, что инстанс слушателя создаётся один раз, а не на каждый рендер.
Часто задаваемые вопросы
Напрямую — нет, потому что useEffect ожидает возврат синхронной функции или undefined. Если коллбэк объявлен как async, он возвращает Promise, который React игнорирует, и механизм очистки перестаёт работать. Решение: вызывай async функцию внутри эффекта: useEffect(() => { async function load() { ... }; load(); }, []).
Ограничения нет. React рекомендует разделять несвязанную логику на отдельные useEffect, даже если у них одинаковый массив зависимостей. Каждый эффект должен отвечать за одну чётко определённую побочную задачу: один для подписки, другой для загрузки данных, третий для синхронизации заголовка вкладки. Это упрощает понимание и отладку.
В React Strict Mode (только режим разработки) все эффекты монтируются, размонтируются и монтируются снова. Это фича, а не баг — React проверяет, корректно ли работает cleanup. Если после размонтирования и повторного монтирования эффект ведёт себя неправильно (например, дублируются подписки), твой cleanup неполный. В продакшене эффект выполняется один раз.
Используй AbortController. Создай controller внутри эффекта, передай controller.signal в fetch, и в cleanup вызови controller.abort(). Если компонент размонтируется до завершения запроса, fetch отменится и setState не вызовется. Это предотвращает race condition и ошибку «Can't perform a React state update on an unmounted component».
useEffect будет выполняться после каждого рендера без исключения. Это означает, что любой setState внутри эффекта вызовет новый рендер → новый эффект → бесконечный цикл. На практике эффект без массива зависимостей почти всегда является ошибкой. Исключения — логирование или синхронизация с внешней системой, где каждый рендер требует синхронизации.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также