Мы уже с вами изучили основы функциональных компонентов и управление состоянием с помощью useState. Но до сих пор наши компоненты были, скажем так, немного «замкнутыми в себе». Они умели хранить данные и отрисовывать интерфейс, но их взаимодействие с внешним миром было сильно ограничено.
А что, если нам нужно получить данные с сервера? Или подписаться на какое-то событие? Или изменить DOM-дерево вручную? Вот именно для таких задач и существует главный герой сегодняшнего урока, это хук useEffect. Это один из самых важных и часто используемых хуков в React и сегодня мы разберем его вдоль и поперек.
Что такое «побочный эффект»?
Давайте начнем с фундаментального понятия, которое лежит в основе этого хука. В программировании и в React в частности, «побочным эффектом» (side effect) называется любое действие, которое выходит за рамки вычисления возвращаемого значения.
Звучит сложно? Давайте упростим. Главная задача React-компонента это принимать props и state и возвращать JSX. Это его «чистая» работа. Все, что не входит в эту работу, считается побочным эффектом.
Представьте себе повара, который готовит блюдо по рецепту (это «чистая» функция). Но если ему нужно позвонить поставщику, чтобы заказать новые продукты или включить духовку, то это уже побочные эффекты. Он делает это не для самого процесса готовки, а для обеспечения условий, в которых эта готовка возможна.
Конкретные примеры побочных эффектов в React:
-
Запросы к API. Получение списка пользователей, постов или любой другой данных с сервера.
-
Работа с таймерами. Использование
setTimeoutилиsetInterval. -
Подписка на события. Например, подписка на глобальные события браузера, такие как изменение размера окна (
resize) или нажатие клавиш (keydown). -
Ручное изменение DOM. Когда тебе нужно напрямую взаимодействовать с элементом на странице (хотя в React это нужно делать аккуратно!).
-
Установка подписок. Например, на WebSocket или внешние стейт-менеджеры.
Проблема в том, что выполнение таких операций непосредственно внутри тела компонента или во время рендера может привести к багам, бесконечным циклам обновлений и несоответствию интерфейса. Представь, что ты делаешь запрос к API в теле компонента. После получения данных ты обновляешь состояние. Это приводит к повторному рендеру компонента… который снова делает запрос к API… и снова обновляет состояние. Бесконечный цикл!
Чтобы избежать этих проблем, React предоставляет нам хук useEffect. Он говорит: «Эй, React, выполни этот мой побочный эффект после того, как ты отрисуешь компонент на экране. Сделай это в безопасное время, чтобы не мешать основному процессу рендеринга».
Обзор хука useEffect и его базовый синтаксис
Хук useEffect это наш надежный инструмент для управления побочными эффектами в функциональных компонентах. Он позволяет нам выполнять код в разные моменты жизненного цикла компонента, после того как компонент появился на странице (монтировался), после того как он обновился и перед тем как он исчезнет со страницы (размонтировался).
Базовый синтаксис выглядит так:
import { useEffect } from 'react'; function MyComponent() { useEffect(() => { // Тело эффекта. // Этот код выполняется после каждого рендера компонента. console.log('Эффект выполнен!'); // Опциональная функция очистки. return () => { // Код очистки. // Этот код выполняется перед следующим выполнением эффекта или при размонтировании компонента. console.log('Очистка эффекта!'); }; }); // <-- Зависимости (второй аргумент) не указаны. return <div>Мой компонент</div>; }
Давай разберем эту конструкцию по частям:
-
Импорт. Первым делом мы импортируем
useEffectиз ‘react’. -
Вызов хука. Мы вызываем
useEffectвнутри нашего функционального компонента. -
Первый аргумент. функция-эффект. Это та самая функция, которая содержит код нашего побочного эффекта. React будет выполнять эту функцию после того, как он обновит DOM и браузер отрисует эти изменения на экране.
-
Второй аргумент, массив зависимостей. Это опциональный (необязательный) параметр, но невероятно важный. Он контролирует, когда именно должен выполняться наш эффект. Мы поговорим о нем подробнее чуть позже.
-
Возврат функции очистки (опционально). Из функции-эффекта мы можем вернуть другую функцию. Эта функция называется «функцией очистки». React будет вызывать ее перед тем, как компонент удалится из DOM, а также перед каждым новым выполнением эффекта (для очистки последствий предыдущего эффекта). Это идеальное место для отмены подписок, таймеров и т.д.
Самое важное, что нужно запомнить на старте: по умолчанию (если не передан второй аргумент), эффект выполняется после каждого рендера компонента. И после первого рендера и после каждого обновления.
Массив зависимостей: контролируем выполнение эффекта
Массив зависимостей это то, что превращает useEffect из простого инструмента в мощный и точный механизм. Он позволяет нам оптимизировать производительность и избежать ненужных срабатываний.
Давай рассмотрим три основных сценария.
1. Эффект без массива зависимостей
Как мы уже сказали, эффект выполняется после каждого рендера.
useEffect(() => { console.log('Этот эффект сработает после первого рендера и после ЛЮБОГО обновления компонента!'); });
Такой подход редко бывает нужен, так как часто наши эффекты требуют большей точности.
2. Эффект с пустым массивом зависимостей []
Это говорит React: «Выполни этот эффект только один раз, после первого рендера (монтирования) компонента». Это самый распространенный случай для операций, которые должны быть выполнены единожды при старте, например, для загрузки данных с сервера.
useEffect(() => { console.log('Этот эффект сработает ТОЛЬКО после первого рендера (аналог componentDidMount).'); }, []); // <-- Пустой массив зависимостей
3. Эффект с массивом зависимостей [dep1, dep2, ...]
Это самый гибкий вариант. Мы говорим React: «Выполняй этот эффект после первого рендера и затем только после тех рендеров, при которых изменилось хотя бы одно значение из этого массива».
const [count, setCount] = useState(0); const [name, setName] = useState('Максим'); useEffect(() => { console.log(`Эффект зависит от count и name. Сейчас count=${count}, name=${name}`); // Этот эффект выполнится после первого рендера и после любого рендера, в котором // изменилось либо значение count, либо значение name. }, [count, name]); // <-- Массив с зависимостями
Представь, что у тебя есть компонент профиля пользователя. Ты можешь сделать эффект, который загружает данные пользователя и он должен выполняться только тогда, когда меняется userId, переданный в пропсах.
function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { // Эта функция будет вызвана при монтировании и каждый раз, когда обновится проп userId. fetch(`/api/users/${userId}`) .then(response => response.json()) .then(userData => setUser(userData)); }, [userId]); // Эффект перезапускается только когда userId меняется if (!user) { return <div>Загрузка...</div>; } return <div>Имя: {user.name}</div>; }
Если бы мы забыли указать [userId] в зависимостях, эффект бы не выполнился при получении нового userId и мы бы всегда видели профиль первого загруженного пользователя. Если бы мы указали пустой массив [], эффект выполнился бы только один раз при монтировании и userId внутри эффекта всегда бы оставался тем, что был при первом рендере.
Важное правило: Все значения из области видимости компонента, которые используются внутри эффекта, должны быть указаны в массиве зависимостей. React поможет тебе с этим, если ты используешь линтер (например, ESLint с плагином eslint-plugin-react-hooks).
Функция очистки
Некоторые побочные эффекты требуют «уборки за собой». Например, если ты установил подписку на событие или запустил таймер, их нужно отменить, когда компонент удаляется из DOM. Если этого не сделать, могут возникнуть утечки памяти или попытки обновить состояние уже несуществующего компонента.
Для этого и существует функция очистки. Её нужно вернуть из функции-эффекта.
useEffect(() => { console.log('Эффект выполнен (подписка установлена).'); // Функция очистки return () => { console.log('Очистка эффекта (подписка отменена).'); // Здесь мы отменяем подписку, таймер и т.д. }; }, []);
Давай рассмотрим практический пример с таймером.
import { useState, useEffect } from 'react'; function Timer() { const [seconds, setSeconds] = useState(0); useEffect(() => { console.log('Запускаем таймер...'); // Устанавливаем интервал, который каждую секунду обновляет seconds const intervalId = setInterval(() => { setSeconds(prevSeconds => prevSeconds + 1); // Используем функциональную форму обновления }, 1000); // Возвращаем функцию очистки return () => { console.log('Останавливаем таймер...'); clearInterval(intervalId); // Очищаем интервал при размонтировании }; }, []); // Пустой массив зависимостей = эффект только при монтировании/размонтировании return <div>Прошло секунд: {seconds}</div>; }
В этом примере:
-
При монтировании компонента
TimerзапускаетсяsetInterval. -
Таймер тикает каждую секунду, обновляя состояние.
-
Когда компонент
Timerудаляется из DOM (например, мы переключаемся на другую страницу), React вызывает функцию очистки, которая останавливает таймер с помощьюclearInterval(intervalId).
Если бы мы не очистили интервал, он бы продолжал тикать в фоне и пытаться обновлять состояние несуществующего компонента, что привело бы к ошибке в консоли.
Эта же логика применима к подписке на события (addEventListener / removeEventListener), запросам к API (где можно отменять запросы с помощью AbortController) и любым другим долгоживущим операциям.
Практические примеры и задачи
Я подготовил несколько примеров и задач для тебя.
Пример 1: Загрузка данных при монтировании
Самый классический пример использования useEffect.
import { useState, useEffect } from 'react'; function PostList() { const [posts, setPosts] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { // Функция для загрузки данных const fetchPosts = async () => { try { setIsLoading(true); const response = await fetch('https://jsonplaceholder.typicode.com/posts'); if (!response.ok) { throw new Error('Ошибка при загрузке данных'); } const postsData = await response.json(); setPosts(postsData); } catch (err) { setError(err.message); } finally { setIsLoading(false); } }; fetchPosts(); }, []); // Загружаем посты только один раз при монтировании if (isLoading) { return <div>Загружаем список постов...</div>; } if (error) { return <div>Ошибка: {error}</div>; } return ( <ul> {posts.map(post => ( <li key={post.id}> <h3>{post.title}</h3> <p>{post.body}</p> </li> ))} </ul> ); }
Задача 1: События resize
Создай компонент WindowSize, который отображает текущую ширину и высоту окна браузера. Он должен обновлять эти значения при изменении размера окна.
Подсказка:
-
Используй
useStateдля храненияwidthиheight. -
В
useEffectс пустым массивом зависимостей подпишись на событиеresizeобъектаwindow. -
В обработчике события обновляй состояние, используя
window.innerWidthиwindow.innerHeight. -
Не забудь вернуть функцию очистки, в которой отписаться от события (
removeEventListener).
Пример 2: Эффект, зависящий от состояния
Создадим простой счетчик, который также меняет заголовок вкладки браузера.
import { useState, useEffect } from 'react'; function CounterWithDocumentTitle() { const [count, setCount] = useState(0); // Эффект, который обновляет заголовок документа useEffect(() => { console.log('Обновляем заголовок...'); document.title = `Вы нажали ${count} раз`; }, [count]); // Эффект зависит от count и выполнится при его изменении return ( <div> <p>Вы нажали {count} раз</p> <button onClick={() => setCount(count + 1)}> Нажми меня </button> </div> ); }
В этом примере заголовок вкладки будет обновляться каждый раз, когда меняется значение count. Обрати внимание, что count указан в массиве зависимостей.
Задача 2: Поиск с дебаунсом (отложенным запросом)
Это более сложная, но очень реальная задача. Создай компонент поиска, который отправляет запрос к API не на каждое нажатие клавиши, а только после того, как пользователь перестал печатать на 500 миллисекунд (техника «debounce»).
Подсказка:
-
Создай состояние
queryдля строки поиска иresultsдля результатов. -
Используй
useEffectс зависимостью[query]. -
Внутри эффекта, установи таймер (
setTimeout) на 500 мс, который будет выполнять запрос к API (или, для простоты, просто обновлятьresultsmock-данными). -
В функции очистки этого эффекта, сбрасывай предыдущий таймер с помощью
clearTimeout. -
Это гарантирует, что запрос будет отправлен только если в течение 500 мс не произошло нового изменения
query.
Пример 3: Комбинирование нескольких эффектов
В одном компоненте можно и нужно использовать несколько хуков useEffect для разделения несвязанной логики.
function FriendStatus({ friendId }) { const [isOnline, setIsOnline] = useState(null); const [windowWidth, setWindowWidth] = useState(window.innerWidth); // Эффект для подписки на статус друга useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } // Здесь должна быть реальная логика подписки (например, из API) console.log(`Подписались на друга с ID: ${friendId}`); // Имитируем подписку const timeoutId = setTimeout(() => { handleStatusChange({ isOnline: true }); }, 1000); // Очистка подписки return () => { console.log(`Отписались от друга с ID: ${friendId}`); clearTimeout(timeoutId); }; }, [friendId]); // Переподписываемся при смене friendId // Эффект для отслеживания ширины окна (независимая логика) useEffect(() => { function handleResize() { setWindowWidth(window.innerWidth); } window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); }; }, []); // Подписка на resize создается и удаляется только один раз return ( <div> <p>Ширина окна: {windowWidth}px</p> <p> {isOnline === null ? 'Загрузка статуса...' : isOnline ? 'Друг онлайн' : 'Друг офлайн'} </p> </div> ); }
Типичные ошибки и лучшие практики
-
Не забывай про массив зависимостей. Использование
useEffectбез второго аргумента или с неправильно указанными зависимостями, это самая частая причина багов. Следуй правилам линтера. -
Не игнорируй функцию очистки. Если твой эффект что-то подписывает, запускает или создает, всегда задавайся вопросом: «А нужно ли мне это отменить/остановить/удалить при размонтировании?». Если ответ «да», используй функцию очистки.
-
Не вызывай хуки внутри условий и циклов. Это правило действует для всех хуков, включая
useEffect. Все хуки должны вызываться на верхнем уровне компонента. -
Не используй
useEffectдля обработки событий. Если какой-то эффект вызван конкретным действием пользователя (например, отправка формы по клику на кнопку), обрабатывай это в обработчике события, а не в эффекте. -
Разделяй несвязанную логику по разным эффектам. Если у тебя есть две независимые операции (как в примере выше со статусом друга и шириной окна), вынеси их в отдельные
useEffect. Так код будет чище и понятнее.
Ты только что прошел один из самых важных уроков во всем нашем курсе. Хук useEffect это сердце функциональных компонентов React и теперь у тебя есть знания, чтобы использовать его эффективно и безопасно.
Если ты хочешь пройти полный курс с уроками по React для начинающих, где мы шаг за шагом разберем все аспекты этой мощной библиотеки, то тебе сюда: Полный курс с уроками по React для начинающих.
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


