Представьте такую сцену: пользователь вашего React-приложения быстро переключается между страницами, пока компоненты в фоне продолжают запрашивать данные. Сервер получает десятки ненужных запросов, интерфейс «дергается», а в консоли мелькает предупреждение: «Can't perform state update on unmounted component». Знакомо? Эта ситуация — лишь верхушка айсберга проблем управления асинхронными операциями в современных веб-приложениях.
Главный враг — утечка состояний
Классическая ошибка новичков выглядит так:
function UserProfile({ userId }) {
const [userData, setUserData] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(data => setUserData(data));
}, [userId]);
return <div>{userData?.name}</div>;
}
При быстром изменении userId
или уходе со страницы компонент размонтируется, но Promise продолжит выполнение. Когда он разрешится, setUserData
попытается обновить несуществующий компонент. Последствия:
- Утечка памяти
- Возможные ошибки выполнения
- Некорректная логика (обновление устаревшего состояния)
AbortController — не просто галочка в чек-листе
Современное решение использует интеграцию с Web API:
useEffect(() => {
const controller = new AbortController();
const loadData = async () => {
try {
const response = await fetch(`/api/users/${userId}`, {
signal: controller.signal
});
const data = await response.json();
setUserData(data);
} catch (err) {
if (err.name !== 'AbortError') {
// Реальная обработка ошибок, а не фантомные исключения
console.error('Fetch failed:', err);
}
}
};
loadData();
return () => controller.abort();
}, [userId]);
Здесь есть нюансы:
AbortController.signal
совместим не только сfetch
, но и сaxios
,fetch-полями
, WebSockets- Прерванные запросы выбрасывают DOMException с именем 'AbortError'
- Отмена работает только для текущих запросов, не влияет на кэшированные данные
Оптимизации уровня предприятия
Для сложных сценариев рассмотрите стратегии:
Дебаунс автозаполнения
Комбинируйте useEffect
с setTimeout/clearTimeout, чтобы избежать лавины запросов:
const [searchTerm, setSearchTerm] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
const handler = setTimeout(() => {
if (searchTerm) {
fetchResults(searchTerm).then(setResults);
}
}, 300);
return () => clearTimeout(handler);
}, [searchTerm]);
Стратегия SWR (Stale-While-Revalidate)
Библиотеки типа react-query используют фоновую перевалидацию данных:
const { data, error } = useQuery(['user', userId], () =>
fetch(`/users/${userId}`).then(res => res.json()), {
staleTime: 60 * 1000 // Перезапрашивать не чаще раза в минуту
});
Отслеживание монтирования
Возможен паттерн с флагом монтирования для legacy-кода:
useEffect(() => {
let isMounted = true;
fetchData().then(data => {
if (isMounted) {
setState(data);
}
});
return () => { isMounted = false; };
}, []);
Когда производительность имеет значение
Кейс: таблица с сортировкой, фильтрацией и пагинацией. Интуитивная реализация запускает новый запрос при каждом изменении параметров, но:
- Отклоненные Promise могут вызывать состояние гонки
- Быстрые клики создают «конфликтующие» запросы
Решение — атомарные обновления с проверкой актуальности:
function useCancellableQuery() {
const lastController = useRef(null);
async function query(url) {
if (lastController.current) {
lastController.current.abort();
}
const controller = new AbortController();
lastController.current = controller;
try {
const response = await fetch(url, { signal: controller.signal });
return await response.json();
} finally {
if (lastController.current === controller) {
lastController.current = null;
}
}
}
return query;
}
Момент истины: что выбрать?
Чистый React
- Подходит для простых случаев
- Полный контроль над потоком выполнения
- Риск изобретения велосипедов
Сторонние библиотеки (react-query, SWR)
- Встроенная кэш-логика
- Автоматический повтор запросов
- Префетчинг и оптимистичные обновления
- Дополнительный размер бандла
Чего нельзя игнорировать
-
При интеграции Web Workers или WebSockets используйте явную отписку:
javascriptuseEffect(() => { const ws = new WebSocket(url); ws.onmessage = handleMessage; return () => ws.close(); }, []);
-
Для анимаций и сложных вычислений применяйте двойную проверку монтирования:
javascriptconst animationFrame = useRef(null); useEffect(() => { let active = true; function tick() { if (!active) return; // Вычисления animationFrame.current = requestAnimationFrame(tick); } tick(); return () => { active = false; cancelAnimationFrame(animationFrame.current); }; }, []);
-
В TypeScript аннотируйте ошибки корректно:
typescriptcatch (err) { if (err instanceof DOMException && err.name === 'AbortError') { // Игнорируем отмену } else { // Обработка реальных ошибок } }
Грамотное управление асинхронными операциями превращает хаотичную цепочку запросов в предсказуемый механизм. Выбор между native-подходом и готовым решением — не вопрос догмы, а прагматичный анализ конкретного кейса. Главное — помнить, что каждая асинхронная операция должна иметь четкий сценарий прерывания. Современный фронтенд — это не только умение запускать процессы, но и искусство их вовремя останавливать.