Представим компонент, в котором нужно сделать запрос данных на сервер. Решение этой задачи в лоб может выглядеть как-то так:
const Example = () => {
const [data, setData] = useState({});
axios.get('/data').then((data) => setData(data));
return (
// Отрисовываем данные
)
};
В этом коде мы делаем запрос данных внутри функции компонента, а затем сохраняем полученные данные в стейт. Проблема этого кода в том, что запрос будет вызываться при каждом рендере.
Нужно запомнить одно простое правило: React сам решает, когда вызвать функцию рендера компонента. Поэтому код внутри функции Example
может вызываться множество раз. В нашем примере произойдет бесконечный цикл:
- В обработчике запроса изменяется стейт
- Изменение стейта вызывает перерисовку компонента
- Перерисовка компонента вызывает
axios.get()
- Снова происходит изменение стейта и так по кругу
Никогда не меняйте состояния в рендерах — с этим правилом мы уже знакомились, изучая концепцию MVC.
Выполнить подобные побочные эффекты помогает встроенный хук useEffect()
. Именно его мы изучим в этом уроке.
Хук useEffect()
заменяет три колбека жизненного цикла:
componentDidMount()
componentDidUpdate()
componentWillUnmount()
Подробнее об их работе можно прочитать в официальной документации.
Начнем с простого примера, в котором используется функция alert()
. Вызов этой функции использует API браузера, поэтому он приносит с собой побочные эффекты:
import React, { useState, useEffect } from 'react';
const Example = () => {
const [count, setCount] = useState(0);
// Работает как componentDidMount и componentDidUpdate вместе взятые
// Запускается после рендера компонента
// Вызывается после каждого клика по кнопке
useEffect(() => {
// Состояние доступно внутри за счет обычной области видимости
alert(`Кликов ${count}`);
});
// На классах мы бы сделали так
// Обратите внимание на дублирование
// componentDidMount() {
// alert(`Кликов: ${count}`);
// }
// componentDidUpdate() {
// alert(`Кликов: ${count}`);
// }
return (
<div>
<p>Вы нажали {count} раз(а)</p>
<button onClick={() => setCount(count + 1)}>
Нажми меня
</button>
</div>
);
};
Ниже пример, в котором меняется фон при каждом клике.
See the Pen js_react_hooks_use_ref by Hexlet (@hexlet) on CodePen.
Колбек, переданный в useEffect()
, отрабатывает после первой отрисовки и каждого обновления компонента. То есть произошло объединение методов componentDidUpdate()
и componentDidMount()
. Такое изменение было сделано ради удобства. Мировая практика использования React показала, что, в основном, эффекты происходят после каждого рендера, независимо от того, первая эта отрисовка или все последующие. Как бонус, сократилось количество дублирования и кода. Какие типичные сайд эффекты встречаются во фронтенде? Например:
- Извлечение данных
- Работа с BOM(Browser Object Model) API, например, Local Storage
- Прямое изменение DOM, сюда же относятся библиотеки не совместимые с React
Действие хука useEffect()
иногда можно пропускать. Такое бывает полезно либо в целях оптимизации, либо, если эффект имеет смысл только при определенных условиях. Для этого в хук вторым аргументом передается массив значений, которые надо отслеживать между отрисовками. Если хотя бы одно значение из этого массива поменялось, то колбек вызывается, если все значения остались прежними — пропускается.
useEffect(() => {
alert(`Кликов ${count}`);
}, [count]);
// Равносильно
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
alert(`Кликов ${count}`);
}
}
То есть колбек будет вызван только тогда, когда изменится count
. Таким же способом можно передать любой набор переменных, который мы бы хотели связать с изменением эффекта. Если хотя бы одна переменная в переданном массиве поменялась, то эффект сработает, иначе React его пропускает.
Что делать, если нужно запустить useEffect()
только на момент первого рендера (сразу после монтирования)? Для этого достаточно передать пустой массив:
// Заменяет собой componentDidMount
useEffect(() => {
alert(`Кликов ${count}`);
}, []);
Решение не самое очевидное, но технически оно не является особым случаем. К нему нужно просто привыкнуть.
Сброс эффекта
В некоторых случаях эффект нужно сбрасывать. Например, когда эффект после изменения пропсов перестает быть актуальным, его нужно «зачистить». Для этого достаточно вернуть функцию из useEffect()
, внутри которой выполняется очистка:
// Предположим, что этот эффект зависит от пропса userId
useEffect(() => {
const id = setTimeout(/* какой-то код с userId */);
return () => clearTimeout(id);
}, [userId]);
Изменение userId приведет к сбросу текущего таймера и установке нового. Подобный код в классах потребовал бы использования аж 4 колбеков жизненного цикла.
Для имитации componentWillUnmount()
достаточно соединить очистку с пустым массивом вторым параметром:
useEffect(() => {
return () => {
// Эта логика выполнится только при размонтировании компонента
};
}, []);
Асинхронные запросы
Первым параметром useEffect()
принимает функцию. Эта функция должна либо ничего не возвращать, либо возвращать функцию для сброса эффекта. Это накладывает некоторое ограничение на использование async await
:
useEffect(async () => {
const data = await axios.get('/todos');
// ...
}, []);
Если использовать async
, то функция уже возвращает промис — это нарушает правило выше. Чтобы этого избежать, можно обернуть асинхронный вызов в функцию и вызвать эту функцию внутри useEffect
:
useEffect(() => {
const requestData = async () => {
const data = await axios.get('/todos');
// ...
};
requestData();
}, []);
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.