React хочет, чтобы ваши компоненты были предсказуемыми. Дайте ему то же состояние (state) и пропсы (props), и он должен отрисовывать один и тот же UI каждый раз. Но большинство реальных приложений не могут выжить внутри этого пузыря. Им нужно взаимодействовать с внешним миром. Панели управления нуждаются в свежих данных с сервера. Виджет чата должен слушать сообщения. Таймер должен тикать. Эти операции являются побочными эффектами (side effects), и они находятся за пределами цикла рендеринга React. Хук useEffect — это место, куда вы выносите эту запутанную, непредсказуемую работу, чтобы сам компонент оставался «чистым».
Побочные эффекты: что должно быть внутри useEffect
Побочный эффект — это всё, что выходит за рамки возврата JSX. Фаза рендеринга React должна быть чистой (pure). Когда вы начинаете загружать данные, записывать значения в глобальные переменные или прикреплять слушателей к DOM, вы выходите за пределы «чистой» территории.
Common examples include:
- Загрузка данных из API
- Настройка таймеров или интервалов
- Добавление слушателей событий (event listeners) к
windowилиdocument - Обновление заголовка вкладки браузера
- Подключение к WebSockets
У этих задач есть одна общая черта: они не должны находиться внутри оператора return вашего компонента или в основной логике рендеринга. Попытка вызвать браузерное API, например setInterval, напрямую внутри тела рендеринга приведет к тому, что он будет срабатывать при каждом рендере, создавая дублирующиеся таймеры и вызывая непредсказуемое поведение. useEffect существует именно для того, чтобы изолировать эту работу и выполнять её в нужный момент.
Как массив зависимостей управляет временем выполнения
Вторым аргументом useEffect является массив зависимостей, и это главный источник путаницы для разработчиков, переходящих с классовых компонентов. Думайте о нем как о наборе переменных, за которыми React следит, чтобы решить, пропустить эффект или выполнить его после текущего рендеринга.
Существует три паттерна, которые вы будете использовать постоянно.
Отсутствие массива зависимостей. Если вы полностью опустите массив, React решит, что эффект должен запускаться после каждого рендеринга, включая самый первый. Это требуется крайне редко. Если ваш эффект выполняет сетевой запрос или тяжелую операцию с DOM, его запуск при каждом нажатии клавиши или изменении состояния убьет производительность. Используйте этот паттерн только тогда, когда вам действительно нужно перезапускать что-то, потому что может измениться любой пропс или состояние, и вы не можете указать конкретные.
Пустой массив []. Это говорит React запустить эффект один раз, сразу после монтирования (mount) компонента и готовности DOM. Это идеальное место для первоначальной загрузки данных. Например, если ваш компонент загружает данные профиля пользователя, вы хотите, чтобы этот запрос выполнился ровно один раз при появлении страницы профиля, а не каждый раз, когда пользователь взаимодействует с формой ниже по странице.
Массив с конкретными переменными [count]. Это инструмент точной настройки. React сравнивает текущие значения этих зависимостей с их значениями во время последнего рендеринга. Если хотя бы одно из них изменилось, эффект запускается. Если в списке ничего не изменилось, React полностью пропускает эффект.
Если вы синхронизируете заголовок вкладки браузера с переменной состояния, вы поместите эту переменную в массив зависимостей. Тогда React будет обновлять заголовок только при изменении этого значения. Если вы её не укажете, заголовок останется устаревшим. Если добавите лишние переменные состояния, вы будете тратить ресурсы на обновление заголовка при изменениях, которые не имеют значения.
Очистка (Cleanup) — это не опция, а необходимость
Некоторые эффекты оставляют после себя «следы». Таймер продолжает считать. Слушатель событий продолжает срабатывать. WebSocket остается открытым. Когда ваш компонент размонтируется (unmount) или даже когда эффект перезапускается из-за изменения зависимостей, React не очищает автоматически остатки предыдущего эффекта. Это ваша задача.
Вы создаете функцию очистки, возвращая функцию изнутри useEffect. React вызывает эту функцию очистки перед тем, как применить следующий эффект, и еще раз, когда компонент исчезает с экрана.
Вам следует использовать очистку для:
- Очистки интервалов или таймаутов с помощью
clearIntervalилиclearTimeout - Удаления слушателей событий, добавленных к
window,documentили внешним узлам - Отписки от потоков данных или сервисов
Пренебрежение этим приводит к утечкам памяти. Компонент монтируется, прикрепляет слушатель прокрутки, размонтируется, а слушатель остается. Браузер удерживает колбэк и узлы DOM, на которые он ссылается. Со временем, особенно в одностраничных приложениях (SPA) с активной навигацией, эти «призраки» накапливаются и замедляют работу вкладки. Решение обычно заключается всего в нескольких строках: верните функцию, которая удаляет то, что вы добавили.
Распространенные ошибки, которые попадают в продакшн
Даже опытные разработчики прибегают к useEffect, когда существует более простой вариант. Вот три паттерна, которые должны стать «тревожным звоночком» во время код-ревью.
Бесконечные циклы. Никогда не обновляйте переменную состояния внутри useEffect, если эта же переменная указана в вашем массиве зависимостей, за исключением случаев, когда у вас есть условие-предохранитель, разрывающее цикл. Если вы считываете count, увеличиваете его и указываете count в зависимостях, React видит изменение, выполняет ререндер, снова запускает эффект, снова увеличивает значение и в итоге блокирует браузер.
Ненужные эффекты. Не используйте useEffect для вычисления значения на основе существующих props или state. Если вы можете вычислить его напрямую во время рендеринга, просто сделайте это. Производные значения должны находиться в теле компонента или в мемоизированном вычислении с помощью useMemo. Перенос их в эффект разделяет вашу логику между фазами рендеринга и эффекта без какой-либо выгоды, что усложняет понимание кода.
Неподходящий инструмент для действий пользователя. `use
