Эффекты и загрузка данных — Примеры

Пример 1 — Таймер, продолжающий работать после исчезновения компонента

Часы запускают интервал при монтировании и никогда его не останавливают, поэтому после закрытия панели консоль продолжает писать, а приложение медленно накапливает таймеры. Покажите очистку и скажите, что ломается без неё.

function Clock() {
  const [now, setNow] = useState(() => Date.now());

  useEffect(() => {
    const id = setInterval(() => setNow(Date.now()), 1000);
  }, []);

  return <time>{new Date(now).toLocaleTimeString()}</time>;
}

Решение

function Clock() {
  const [now, setNow] = useState(() => Date.now());

  useEffect(() => {
    const id = setInterval(() => setNow(Date.now()), 1000);
    return () => clearInterval(id);
  }, []);

  return <time>{new Date(now).toLocaleTimeString()}</time>;
}

Функция, которую возвращает эффект, выполняется перед следующим запуском и ещё раз, когда компонент размонтируется, — именно она останавливает всё, что эффект запустил. Без неё интервал переживает компонент и продолжает вызывать сеттер, принадлежащий рендеру, которого больше нет на экране: в худшем случае таймер, сокет или подписка создаются при каждом монтировании и никогда не освобождаются, поэтому панель, которую открывают и закрывают весь день, оставляет за собой растущую груду. Вот почему React к тому же выполняет эффекты дважды в разработке: продублированная подписка вскрывает отсутствующую очистку, а не ошибку фреймворка.

Пример 2 — Поиск, в котором побеждает более медленный ответ

Ввод запускает запрос на каждое нажатие клавиши, и результаты иногда относятся к более раннему запросу. Покажите очистку, которая это исправляет; загрузчик передаётся снаружи, чтобы фрагмент оставался самодостаточным.

function Results({ query, load }) {
  const [state, setState] = useState({ status: "idle" });

  useEffect(() => {
    setState({ status: "loading" });
    load(query).then((data) => setState({ status: "ready", data }));
  }, [query, load]);
}

Решение

function Results({ query, load }) {
  const [state, setState] = useState({ status: "idle" });

  useEffect(() => {
    let ignore = false;
    setState({ status: "loading" });

    load(query)
      .then((data) => { if (!ignore) setState({ status: "ready", data }); })
      .catch(() => { if (!ignore) setState({ status: "error" }); });

    return () => { ignore = true; };
  }, [query, load]);
}

Запросы, запущенные вплотную друг за другом, могут завершиться не по порядку, поэтому без защиты ранний ответ приходит последним и перезаписывает поздний — и список показывает результаты запроса, который пользователь уже заменил. Очистка выполняется перед следующим эффектом, поэтому установленный в ней флаг ignore делает каждый вытесненный ответ пустышкой; отмена самого запроса — лучшее исправление там, где загрузчик её поддерживает, поскольку освобождает не только состояние, но и соединение. Если массив зависимостей не указать, эффект выполнялся бы заново после каждого рендера и запускал новый запрос каждый раз.