Эффекты и загрузка данных
Что это такое
Эффект — это функция, которая выполняется после того, как компонент отрисован; так компонент синхронизируют с чем-то вне React — сетевым запросом, таймером, подпиской или заголовком документа. useEffect принимает эту функцию и массив зависимостей, который говорит, когда запускать её снова.
Почему это важно
Рендеринг обязан оставаться чистым, поэтому всё, что касается внешнего мира, происходит после рендера, а не во время него; в эффектах же живут загрузка данных, подписки и ручная работа с DOM, и там же рождается большинство продакшен-ошибок React. Запрос, который завершается после размонтирования компонента; два запроса, где побеждает более медленный; эффект, который выполняется на каждом рендере и перезапрашивает данные бесконечно, — всё это следствие массива зависимостей, который говорит не то, что имел в виду автор. Освоив правила, вы научитесь читать этот класс ошибок прямо на экране.
Ключевые идеи
Эффекты выполняются после рендера, а не во время него
Тело эффекта выполняется, когда браузер уже нарисовал зафиксированный результат. Порядок здесь намеренный: интерфейс уже на экране, и эффект может спокойно начать медленную работу. Не вычисляйте эффектом то, что и так есть во время рендера, — для этого служит обычный код в теле компонента.
function DocumentTitle({ title }) {
useEffect(() => {
document.title = title;
}, [title]);
return null;
}
Массив зависимостей — это триггер
Всё, что эффект читает из области видимости компонента, должно попасть в массив, а каждую запись React сравнивает с предыдущим рендером через Object.is: любое изменение запускает эффект заново. Пустой массив означает «выполнить один раз после первого рендера», а его отсутствие — «выполнять после каждого рендера»; второе нужно почти никогда.
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
Возвращайте функцию очистки для всего, что запускаете
Функция, которую вернул эффект, выполняется перед следующим запуском и ещё раз, когда компонент размонтируется. Очистка таймера, закрытие сокета и отмена запроса — всё это здесь. Без очистки эффект предыдущего рендера продолжает работать со значениями, которых больше нет.
Загрузке нужна защита от гонки
Два запроса, запущенных подряд, могут завершиться в обратном порядке, и тогда старый ответ перезапишет новый. Лечится это так: ответ игнорируют, как только его эффект очищен. Сам запрос отменяет AbortController, а флаг ignore работает там, где отменить нельзя.
useEffect(() => {
let ignore = false;
setState({ status: "loading" });
fetch(`/api/search?q=${encodeURIComponent(query)}`)
.then((response) => response.json())
.then((data) => {
if (!ignore) setState({ status: "ready", data });
})
.catch(() => {
if (!ignore) setState({ status: "error" });
});
return () => {
ignore = true;
};
}, [query]);
Моделируйте запрос как небольшой конечный автомат — loading, ready, error — а не как три булевых значения, которые могут противоречить друг другу.
Эффект, запускающий одно и то же дважды, — это запах
В режиме разработки React выполняет эффекты дважды, чтобы вскрыть отсутствующую очистку. Если из-за этого появляется вторая подписка или удваивается счётчик, настоящая ошибка — в отсутствующей очистке, а не в двойном запуске. Исправляйте эффект, а второй запуск через ref не подавляйте.
Практика
Загрузите список с состоянием загрузки и ошибки
Загрузите список с выбранного вами эндпоинта и отрисуйте три состояния: индикатор загрузки, список и сообщение с кнопкой повтора. Добавьте поле поиска, меняющее запрос.
Готово, когда: индикатор появляется при первой загрузке, неудачный запрос показывает сообщение об ошибке с рабочей кнопкой повтора, а быстрый ввод никогда не оставляет список с результатами предыдущего запроса.
Докажите, что очистка выполняется
Добавьте компонент, который пишет в журнал при монтировании и при размонтировании. Смонтируйте и размонтируйте его кнопкой-переключателем, а затем смонтируйте сразу два таких компонента.
Готово, когда: консоль показывает один запуск и одну остановку для каждого монтирования, ни одна остановка не пропущена и ни одна не продублирована.
Частые ошибки
- Забытый массив зависимостей — эффект выполняется после каждого рендера и перезапрашивает данные в цикле; исправить: перечислите каждое читаемое значение или используйте пустой массив, если оно не читает ничего.
- Забытая очистка — таймеры, сокеты и подписки продолжают работать после размонтирования; исправить: возвращайте функцию, останавливающую всё, что запустил эффект.
- Игнорирование ответов, пришедших не в том порядке — медленный ранний запрос перезаписывает быстрый поздний; исправить: отменяйте через
AbortControllerили защищайтесь флагомignore, устанавливаемым в очистке. - Вычисление в эффекте того, что относится к рендеру — лишний рендер и мигание устаревшего результата; исправить: считайте это в теле компонента и передавайте вниз.
- Один эффект на несвязанную работу — массив зависимостей смешивает заботы, и несвязанные запуски триггерят друг друга; исправить: разделите на один эффект на каждую синхронизацию.
Что читать дальше
- Состояние и хуки (02-frontend/29-react/03-state-and-hooks) — правило снимка объясняет, почему эффект всё ещё может прочитать устаревшее значение; перечитайте его перед отладкой запроса.
- Веб-производительность (Core Web Vitals) (02-frontend/26-web-performance-core-web-vitals) — сколько запросов и работы должно следовать за первым рендером.
- Инструменты разработчика в браузере (02-frontend/27-browser-devtools) — как наблюдать зависший запрос и размонтированный компонент рядом друг с другом.