Эффекты и загрузка данных — Примеры
Пример 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 делает каждый вытесненный ответ пустышкой; отмена самого запроса — лучшее исправление там, где загрузчик её поддерживает, поскольку освобождает не только состояние, но и соединение. Если массив зависимостей не указать, эффект выполнялся бы заново после каждого рендера и запускал новый запрос каждый раз.