useEffect запускает кусок кода после того, как React отрендерил компонент и обновил экран. Его используют, чтобы держать компонент в синхронизации с чем-то вне React, например таймером, сетевым соединением или обработчиком событий, а когда он запускается снова, вы управляете массивом зависимостей.
Откройте консоль под превью: эффект выводит сообщение один раз при появлении компонента, а затем снова после каждого клика. Замените [count] на [] и нажмите снова: после первого сообщения эффект молчит.
Синтаксис
useEffect(() => {
// setup: runs after the render is on the screen
return () => {
// cleanup: runs before the next setup, and on unmount
};
}, [dependencies]);
- Первый аргумент это функция настройки (setup). Она может вернуть функцию очистки (cleanup) или ничего.
- Второй аргумент это массив зависимостей: каждое значение из компонента, которое читает setup (пропсы, состояние и всё, что из них вычислено).
- Вызывайте
useEffectна верхнем уровне компонента, как любой хук.
Эффекты выполняются после рендера
Рендеринг это вызов функции вашего компонента React, чтобы узнать, что должно быть на экране. Эффекты ждут, пока эта работа закончится и DOM обновится, поэтому внутри эффекта страница уже показывает новые значения.
Каждый клик сначала выводит рендер, а потом эффект, и эффект читает текст кнопки, который React только что записал. Поэтому чтение DOM, запуск таймера или подключение к серверу место в эффекте, а не в теле компонента: тело выполняется во время рендеринга, когда на экране ещё ничего нет, и React может вызывать его чаще, чем вы ожидаете.
Эффекты обычно выполняются после того, как браузер отрисовал страницу. Когда эффекту нужно измерить DOM до отрисовки, используйте вместо него useLayoutEffect, который всегда выполняется до отрисовки.
Массив зависимостей: три формы
Массив зависимостей решает, когда эффект запускается снова. Форм три, и этот блок использует все, чтобы вы видели разницу.
При загрузке все три эффекта выводят сообщения. Нажмите a + 1, и запустятся два: без массива и зависящий от a. Нажмите b + 1, и запустится только эффект без массива, потому что в [] и [a] ничего не изменилось.
| Массив зависимостей | Когда выполняется эффект |
|---|---|
| нет | После каждого рендера |
[] | Один раз, после первого рендера (при монтировании) |
[a, b] | После первого рендера, затем при каждом изменении a или b |
React сравнивает каждую зависимость с предыдущим значением через Object.is. Числа, строки и булевы значения сравниваются по значению. Объекты, массивы и функции сравниваются по идентичности, и это очень важно (см. ниже).
Не убирайте значение из массива, чтобы эффект запускался реже. Эффект продолжит использовать значение из старого рендера. Это ловит правило линтера react-hooks/exhaustive-deps; оно входит в настройку ESLint, которую включают шаблон React для Vite и конфигурация ESLint в Next.js.
Очистка
Многие эффекты запускают что-то, что потом нужно остановить: соединение, подписку, таймер. Верните функцию из эффекта, и React вызовет её в два момента: прямо перед повторным запуском эффекта с новыми зависимостями и когда компонент уходит с экрана (размонтирование).
Выберите другую комнату: консоль покажет disconnect from general, а затем connect to music. Старое соединение всегда закрывается до того, как открывается новое. Нажмите Hide, и очистка выполнится в последний раз, потому что ChatRoom размонтирован.
Функция connect здесь заменяет настоящий клиент чата или WebSocket. Форма одинакова для любого из них: создайте его в setup, закройте в cleanup.
Очистка таймера
Таймер, запущенный в эффекте, продолжает работать после исчезновения компонента, если очистка его не остановит.
Удалите строку clearInterval(id);, затем скройте таймер: tick продолжает приходить в консоль от компонента, которого больше нет. Каждый Show запускает ещё один.
Обратите внимание на setSeconds((s) => s + 1). Эффект запускается один раз, поэтому простой setSeconds(seconds + 1) навсегда читал бы seconds из первого рендера и остановился бы на 1. Функция обновления всегда получает последнее значение.
Удаление обработчика событий
Обработчики событий на window или document следуют тому же шаблону. Передайте в removeEventListener ту же функцию, что передавали в addEventListener:
useEffect(() => {
function handleKeyDown(e) {
if (e.key === 'Escape') onClose();
}
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onClose]);
Бесконечный цикл
Эффект, который устанавливает значение состояния, от которого сам зависит, запускает цикл: состояние меняется, React рендерит, зависимость изменилась, эффект запускается и снова устанавливает состояние.
// Never stops: every render runs the effect, every effect causes a render
useEffect(() => {
setCount(count + 1);
});
// Same loop: count changes, so the effect runs again
useEffect(() => {
setCount(count + 1);
}, [count]);
Компонент рендерится бесконечно, и вкладка остаётся занятой. React этот цикл не останавливает: в разработке он выводит в консоль ошибку "Maximum update depth exceeded", а в продакшене ничего не выводит. Этот блок запускает вторую версию с условием остановки, чтобы вы могли посмотреть на цикл, не замораживая страницу:
Консоль выводит от render 0 до render 5: шесть рендеров на одно монтирование, каждый запущен эффектом. Решение зависит от того, что вы имели в виду:
- Вы хотели один раз. Используйте
[]и форму с функцией обновления,setCount((c) => c + 1), чтобыcountне был зависимостью. - Вы вычисляете значение из другого состояния. Вообще его не храните; вычисляйте во время рендера, как показано в последнем разделе этой страницы.
- Вы реагируете на действие пользователя. Поместите код в обработчик события.
Объекты и функции как зависимости
Объект, массив или функция, созданные в теле компонента, при каждом рендере становятся новым значением, поэтому эффект, который от них зависит, запускается после каждого рендера, даже если внутри ничего не изменилось.
Переключение цветов не имеет никакого отношения к поиску, но каждое переключение снова выводит search for react. Перенесите строку options внутрь эффекта и замените массив на [query]: переключения затихнут, и поиск будет запускать только ввод в поле.
Это общее решение. Создавайте объекты и функции внутри эффекта, который их использует, и зависьте от примитивных значений, из которых они собраны. Когда функция должна жить вне эффекта, useCallback сохраняет её идентичность между рендерами.
Почему эффекты выполняются дважды в разработке
При включённом <StrictMode> (новое приложение на Vite оборачивает в него корень, а App Router в Next.js включает его по умолчанию) React в разработке проводит проверку: монтирует каждый компонент, запускает его эффекты, запускает их очистки и снова запускает эффекты. Для чата выше консоль разработки показывает:
connect to general
disconnect from general
connect to general
Это происходит один раз, при монтировании, и только в разработке. Проверка нужна, чтобы выявить эффекты без очистки: если два запуска подряд что-то ломают, то пользователь сломает это тоже, уйдя и вернувшись. Решение никогда не в том, чтобы блокировать второй запуск через ref; оно в том, чтобы написать очистку. Превью на этой странице работает как продакшен-сборка, поэтому двойного запуска здесь вы не увидите.
Возможно, эффект вам не нужен
Эффекты нужны для синхронизации с системами вне React. Два частых применения лучше обходятся без них.
Значения, вычисляемые из пропсов или состояния. Вычисляйте их во время рендера. Хранение их в состоянии и синхронизация эффектом стоят лишнего рендера, а первый из этих рендеров помещает в DOM устаревшее значение:
Каждый клик рендерит WithEffect дважды, и его первый рендер всё ещё показывает старое имя. Computed рендерится один раз с правильным значением.
Код, который реагирует на действие пользователя. Отправка формы, всплывающее уведомление после покупки или отправка аналитического события по клику место в обработчике события. Там вы точно знаете, что произошло, а эффект знает только, что изменилось какое-то значение.
useEffectEvent
Иногда эффекту нужно последнее значение пропса, но без повторного запуска при его изменении. В чате уведомление должно использовать текущую theme, но смена темы не должна переподключать. Для этого React 19.2 добавляет useEffectEvent:
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme); // always the latest theme
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', () => onConnected());
connection.connect();
return () => connection.disconnect();
}, [roomId]); // theme is not a dependency
}
Функцию из useEffectEvent можно вызывать только внутри эффектов. Не передавайте её в другие компоненты и не указывайте в массиве зависимостей.
Часто задаваемые вопросы
Когда выполняется useEffect?
После того как React отрендерил компонент и обновил DOM, обычно после того, как браузер отрисовал страницу. Без массива зависимостей он выполняется после каждого рендера, с [] только после первого, а с [a, b] после первого рендера и после каждого рендера, в котором изменились a или b.
Что такое функция очистки useEffect?
Функция, которую вы возвращаете из эффекта. React вызывает её перед повторным запуском эффекта с новыми зависимостями и ещё раз, когда компонент убирается с экрана. Используйте её, чтобы очищать таймеры, удалять обработчики событий и закрывать подписки.
Почему мой useEffect выполняется дважды?
В разработке <StrictMode> монтирует каждый компонент, запускает его эффекты, очищает их и запускает снова, чтобы показать эффекты без функции очистки. Это происходит один раз при монтировании и только в разработке. Продакшен-сборки запускают эффект один раз.
Почему мой useEffect вызывает бесконечный цикл?
Эффект устанавливает значение состояния, которое также есть в его массиве зависимостей (или массива нет вовсе). Установка состояния вызывает новый рендер, зависимость изменилась, и эффект запускается снова. Используйте функцию обновления и уберите зависимость, добавьте условие или вычисляйте значение во время рендера.
Может ли колбэк useEffect быть async?
Нет. Колбэк должен возвращать ничего или функцию очистки, а функция async возвращает промис. Объявите асинхронную функцию внутри эффекта и вызовите её, как показано на странице о загрузке данных.