Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Хук useEffect JS: React Hooks

Представим компонент, в котором нужно сделать запрос данных на сервер. Решение этой задачи в лоб может выглядеть как-то так:

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();
}, []);

Дополнительные материалы

  1. Использование хука useEffect
  2. Почему эффекты выполняются при каждом обновлении

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 6 300 ₽ в месяц
Разработка фронтенд-компонентов для веб-приложений
10 месяцев
с нуля
Старт 25 апреля
профессия
от 9 900 ₽ в месяц
Разработка фронтенд- и бэкенд-компонентов для веб-приложений
16 месяцев
с нуля
Старт 25 апреля

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»