React
useEffect: синхронизация с внешним миром
Чат-комната подключается к серверу при открытии и должна отключиться при уходе. React ничего не знает про WebSocket - это система вне его мира. Если дёрнуть connect() прямо в теле компонента, соединение откроется на каждом рендере, и через минуту их будут сотни. Команды Figma, Linear и Discord держат тысячи живых соединений на клиенте, и весь этот мост между React и внешним миром строится одним хуком - useEffect.
- Discord: подписка на gateway-сокет и обновление presence живут в эффектах, которые отключаются при смене канала
- Figma и Linear: real-time синхронизация документа поднимает соединение в эффекте и закрывает его в cleanup при уходе со страницы
- Любой дашборд: подписка на window resize, scroll или matchMedia для адаптивной верстки - классический эффект с очисткой
- Аналитика: отправка события 'экран просмотрен' при появлении компонента - побочный эффект, который React сам по себе не делает
- Интеграции: инициализация чужих библиотек (карта Leaflet, плеер video.js, редактор CodeMirror), которые работают с DOM напрямую
Предварительные знания
- Модель рендера React: компонент - чистая функция, которая вызывается заново при изменении состояния
- useState и понимание того, что значения props и state свежие на каждый рендер
- Базовое представление о браузерных API: addEventListener, setInterval, WebSocket, fetch
Эффект синхронизирует, а не 'выполняется после'
Распространённое заблуждение: useEffect - это место для кода, который должен выполниться после рендера. Точнее думать иначе. Эффект описывает, как привести внешнюю систему в соответствие с текущим состоянием компонента. Внешняя система - это всё, чем React не управляет напрямую: WebSocket, таймер, подписка на событие окна, чужая библиотека, которая сама пишет в DOM. Рендер у React чистый: он лишь возвращает описание UI и не должен трогать внешний мир. Эффект - это легальное место для такого взаимодействия.
Здесь эффект говорит: 'пока компонент показывает комнату roomId, должно существовать соединение с этой комнатой'. Когда roomId меняется, React сам разорвёт старое соединение и поднимет новое. Разработчик не пишет 'при клике подключись' - он описывает желаемое состояние внешней системы, а согласование берёт на себя React.
Эффект запускается после коммита и после paint - то есть после того, как браузер уже показал новый кадр. Это сделано намеренно: подписки и сетевые вызовы не должны задерживать отрисовку. Если же нужно измерить DOM и поправить его до того, как пользователь увидит мерцание, для этого есть useLayoutEffect, который выполняется до paint.
Как точнее всего описать назначение useEffect?
Массив зависимостей и функция очистки
Второй аргумент useEffect - массив зависимостей. В нём перечисляют все реактивные значения, которые эффект читает: props, state, и всё, что из них выведено. React сравнивает массив с предыдущим рендером поэлементно (через Object.is). Если хоть один элемент изменился, React запускает функцию очистки старого эффекта и затем тело нового. Пустой массив означает 'нет реактивных зависимостей, синхронизировать нечего, кроме как один раз'. Отсутствие массива означает 'перезапускать после каждого рендера'.
| Зависимости | Когда запускается эффект | Когда чистится |
|---|---|---|
| Нет массива | После каждого рендера | Перед каждым следующим запуском и при размонтировании |
| [] пустой | Один раз после первого коммита | Один раз при размонтировании |
| [a, b] | Первый раз и когда a или b изменились | Перед перезапуском и при размонтировании |
Функция очистки - это return из эффекта. Её задача - отменить ровно то, что сделало тело эффекта: разорвать соединение, снять слушатель, очистить таймер, отписаться. React вызывает её перед каждым повторным запуском эффекта и при размонтировании компонента. Без очистки подписки накапливаются: при каждом изменении зависимостей повисает новый слушатель, а старый продолжает жить - классическая утечка памяти и источник дублирующихся срабатываний.
В Strict Mode во время разработки React намеренно монтирует, размонтирует и снова монтирует компонент, запуская эффект - очистку - эффект. Это не баг, а проверка: если после двойного прогона соединение задвоилось или слушатель повис, значит очистка написана неверно. В проде двойного прогона нет.
Эффект подписывается на событие, но не возвращает функцию очистки. Что произойдёт при изменении зависимости?
Когда эффект не нужен
Эффекты часто применяют там, где они вредят. Два самых частых случая. Первый: производное состояние. Если значение можно вычислить из существующих props и state, его считают прямо при рендере, а не хранят в отдельном useState, который обновляется эффектом. Прогон через эффект даёт лишний рендер и риск рассинхрона. Второй: реакция на действие пользователя. Логику клика или сабмита пишут в обработчике события, а не в эффекте, который следит за изменением состояния.
- Антипаттерн: производное состояние через эффект — const [items] = useState(...); const [count, setCount] = useState(0); useEffect(() => setCount(items.length), [items]); Это лишний стейт и лишний рендер. Каждое изменение items вызывает второй проход рендера ради count
- Правильно: вычислить при рендере — const [items] = useState(...); const count = items.length; Значение всегда согласовано с items по определению, нет дополнительного рендера и нечему рассинхронизироваться
Полезный фильтр: эффект оправдан, когда синхронизация нужна просто потому, что компонент отрисован с таким состоянием, и сохраняется, пока он на экране. Подписка на сокет, слушатель окна, инициализация чужой библиотеки. Если же речь про 'случилось конкретное событие' или 'значение выводится из других значений' - эффект почти наверняка лишний.
Загрузка данных - пограничный случай. Сделать fetch в эффекте можно, но в проде в 2026 году данные обычно тянут через фреймворк (Server Components, loader роутера) или библиотеку вроде TanStack Query, которая сама решает кеш, гонки запросов и повторные попытки. Ручной fetch в useEffect быстро упирается в эти проблемы.
Компонент хранит список todos в state. Нужно показать число невыполненных задач. Как сделать правильно?
Связь с другими темами
Этот урок вводит эффект как мост во внешний мир. Курс развивает тему дальше:
- Жизненный цикл эффекта — Следующий шаг: думать про эффект как пару synchronize+cleanup, а не mount/unmount, и читать свежие значения через useEffectEvent
- Свои хуки — Логику подписки выносят в useXxx, чтобы повторно использовать её между компонентами
- Модель рендера — Эффект - часть цикла рендер-коммит-эффект, без этой модели его поведение трудно предсказать
Итог
- useEffect синхронизирует компонент с системами вне React: подписки, сетевые соединения, таймеры, чужой DOM
- Эффект запускается после того, как React закоммитил изменения в DOM и браузер отрисовал кадр (после paint), чтобы не блокировать показ
- Массив зависимостей определяет, когда эффект перезапускается: при изменении любого значения из массива React сначала чистит старый эффект, затем запускает новый
- Функция очистки (return из эффекта) отменяет то, что эффект сделал: закрывает соединение, снимает слушатель, чистит таймер
- Эффект не нужен для производного состояния и для обработки событий пользователя - это частая ошибка, ведущая к лишним рендерам
Связанные уроки
- rc-12-effect-lifecycle — Поняв синхронизацию и cleanup, дальше разбираем жизненный цикл эффекта и useEffectEvent
- rc-16-custom-hooks — Логику эффектов чаще всего и выносят в собственные хуки вроде useChatRoom или useOnlineStatus
- rc-10-render-mental-model — Эффект запускается как часть цикла рендера-коммита, поэтому без модели рендера он кажется магией