Как правильно обрабатывать асинхронные функции в useEffect и почему нельзя делать колбэк асинхронным
Что проверяют
Знаете ли вы, что эффект возвращает функцию очистки, и умеете ли отменять устаревший запрос.
Как отвечать
Причина запрета простая: колбэк эффекта должен вернуть либо undefined, либо функцию очистки. Функция с async всегда возвращает промис, React примет его за очистку, вызвать не сможет и предупредит. Отписки и отмены при этом не будет вовсе.
Правильно объявить асинхронную функцию внутри и вызвать её:
useEffect(() => {
const controller = new AbortController();
(async () => {
try {
const res = await fetch(url, { signal: controller.signal });
setData(await res.json());
} catch (e) {
if (e.name !== "AbortError") setError(e);
}
})();
return () => controller.abort();
}, [url]);Отмена тут не украшение, а суть. Без неё две проблемы:
- гонка ответов: пользователь быстро сменил фильтр, ушло два запроса, и медленный первый ответ придёт последним, перезаписав правильные данные. Порядок ответов не гарантирован;
- обновление размонтированного компонента: результат приходит, когда экрана уже нет.
Если запрос отменить нельзя, используют флаг:
let ignore = false;
load().then((d) => { if (!ignore) setData(d); });
return () => { ignore = true; };Стоит добавить, что в режиме строгой проверки React намеренно монтирует компонент дважды в разработке, и без очистки это видно как два запроса подряд. Это не баг, а проверка, что очистка написана.
И честная оговорка: загрузка данных руками в эффекте это самый хлопотный способ. В реальном приложении берут TanStack Query или загрузку на уровне маршрута, где отмена, кэш, повтор и дедупликация уже сделаны.
Где спотыкаются
Пишут useEffect(async () => ...).
Не отменяют запрос и ловят гонку при быстрой смене зависимостей.
Проверяют «смонтирован ли компонент» вместо отмены самого запроса.
Кладут в зависимости объект, который создаётся заново на каждом рендере.