Ошибка React: «Can't perform a React state update on an unmounted component» в TypeScript
Почему возникает ошибка и как её исправить
Ошибка «Can't perform a React state update on an unmounted component» возникает, когда асинхронная операция (например, fetch, setTimeout или debounced callback) пытается обновить состояние компонента после того, как он был удалён из DOM. React считает это утечкой памяти и потенциальным багом, поэтому выводит предупреждение.
Основное решение: флаг mounted или AbortController
В TypeScript лучший способ — использовать AbortController. Это современный API для отмены fetch-запросов и других асинхронных операций. Вот исправленный код:
import React, { useState, useEffect, useRef } from 'react';
interface UserData {
id: number;
name: string;
}
const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
const [user, setUser] = useState<UserData | null>(null);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
const abortController = new AbortController();
const fetchUser = async () => {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`, {
signal: abortController.signal, // передаём сигнал
});
if (!response.ok) throw new Error('Ошибка сети');
const data: UserData = await response.json();
// Проверка не обязательна, так как при отмене fetch выбросит исключение
setUser(data);
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') {
console.log('Запрос отменён — компонент размонтирован');
return; // Не обновляем состояние
}
console.error('Ошибка загрузки:', error);
} finally {
if (!abortController.signal.aborted) {
setLoading(false);
}
}
};
fetchUser();
// Очистка: отменяем запрос при размонтировании
return () => abortController.abort();
}, [userId]);
if (loading) return <div>Загрузка...</div>;
if (!user) return <div>Пользователь не найден</div>;
return <div>{user.name}</div>;
};
Как работает AbortController
- new AbortController() создаёт контроллер с сигналом
signal. - Передаём
signalв fetch как опцию. - В
useEffectcleanup вызываемabortController.abort(). - При отмене fetch выбрасывает исключение
AbortError— мы его ловим и игнорируем. - Это гарантирует, что
setUserиsetLoadingне вызовутся после размонтирования.
Альтернатива: флаг isMounted (устаревший подход)
Раньше использовали флаг, но это менее надёжно:
useEffect(() => {
let isMounted = true;
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (isMounted) setUser(data);
})
.catch(err => console.error(err))
.finally(() => {
if (isMounted) setLoading(false);
});
return () => { isMounted = false; };
}, [userId]);
Недостаток: запрос всё равно выполняется, тратится трафик. AbortController отменяет его на уровне сети.
Работа с Lodash debounce
Если вы используете debounce (например, для поиска), нужно отменять вызов при размонтировании:
import { debounce } from 'lodash';
const SearchComponent: React.FC = () => {
const [query, setQuery] = useState('');
const debouncedSearch = useRef(
debounce((value: string) => {
// Здесь может быть fetch с AbortController
console.log('Поиск:', value);
}, 300)
).current;
useEffect(() => {
return () => debouncedSearch.cancel(); // Отменяем при размонтировании
}, [debouncedSearch]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
};
Используйте useRef, чтобы debounce-функция сохранялась между рендерами. Вызов .cancel() в cleanup предотвращает выполнение после размонтирования.
Рекомендация для TypeScript
- Всегда типизируйте
AbortController— он встроен в lib: "dom". - Для сложных случаев (несколько запросов) используйте отдельные контроллеры.
- Рассмотрите библиотеку
react-queryилиswr— они автоматически управляют отменой запросов.
Исправление этой ошибки делает приложение стабильнее и убирает предупреждения из консоли.
<<Дополнительный подход: кастомный хук useSafeState
Если вы не хотите каждый раз писать логику с AbortController, можно создать обёртку для useState, которая проверяет, смонтирован ли компонент:
import { useState, useEffect, useRef, useCallback } from 'react';
function useSafeState<T>(initial: T): [T, React.Dispatch<React.SetStateAction<T>>] {
const [state, setState] = useState(initial);
const isMountedRef = useRef(true);
useEffect(() => {
return () => { isMountedRef.current = false; };
}, []);
const safeSetState = useCallback((value: React.SetStateAction<T>) => {
if (isMountedRef.current) {
setState(value);
}
}, []);
return [state, safeSetState];
}
// Использование:
const [user, setUser] = useSafeState<UserData | null>(null);
const [loading, setLoading] = useSafeState(true);
Этот хук автоматически предотвращает обновление состояния после размонтирования. Минус: запрос всё равно выполняется. Для простых случаев (не критические данные) этого достаточно. Для fetch с отменой лучше использовать AbortController.
Совмещение с AbortController
Можно комбинировать: useSafeState + AbortController для полной защиты. Но обычно достаточно одного из подходов. Если у вас много асинхронных операций в разных компонентах — useSafeState сократит дублирование кода.
Также рекомендую настроить ESLint правило react-hooks/exhaustive-deps — оно поможет не забыть зависимости в useEffect.