Знакомый баг, который бесит каждого React-разработчика
Вы когда-нибудь видели в консоли браузера предупреждение Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application.? Если да — вы не одиноки. Этот вопрос на Stack Overflow собрал тысячи просмотров и стал настоящей головной болью для новичков и опытных разработчиков на React.
В этой статье мы разберёмся, почему возникает эта ошибка, как её исправить с помощью TypeScript, и главное — как оптимизировать производительность вашего приложения, чтобы подобные предупреждения больше не появлялись. Поехали!
Почему React кричит об unmounted component?
Представьте: пользователь открыл страницу, начал загрузку данных (например, через fetch или axios), но не дождался ответа и ушёл на другой экран. Компонент, который инициировал запрос, уже размонтирован (удалён из DOM), но асинхронная операция всё ещё выполняется. Когда ответ приходит, код пытается обновить состояние (state) уже несуществующего компонента. React справедливо возмущается: «Эй, я не могу обновить то, чего нет!».
В TypeScript эта проблема особенно заметна, если вы используете строгую типизацию и забываете про cleanup-функции в useEffect.
Классический пример проблемы (TypeScript)
import React, { useState, useEffect } from 'react';
interface User {
id: number;
name: string;
}
const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data)); // Ошибка! Компонент может быть размонтирован
}, [userId]);
return <div>{user?.name}</div>;
};
Если пользователь быстро переключит роут, компонент UserProfile будет удалён, а setUser вызовется на пустом месте. Получаем утечку памяти и ошибку в консоли.
3 способа исправить ошибку (с примерами на TypeScript)
Давайте рассмотрим три надёжных подхода, которые решают проблему и улучшают производительность React-приложения.
Способ 1: Флаг с использованием useRef (самый популярный)
Создаём ref-флаг, который показывает, смонтирован ли компонент. Обновляем состояние только если флаг true.
import React, { useState, useEffect, useRef } from 'react';
const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
const [user, setUser] = useState<User | null>(null);
const isMounted = useRef(true);
useEffect(() => {
// Сбрасываем флаг при каждом монтировании
isMounted.current = true;
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (isMounted.current) {
setUser(data);
}
});
// Cleanup: устанавливаем флаг в false при размонтировании
return () => {
isMounted.current = false;
};
}, [userId]);
return <div>{user?.name}</div>;
};
Плюсы: простота, не требует дополнительных библиотек. Минус: нужно не забыть про cleanup.
Способ 2: AbortController для отмены fetch-запроса (рекомендуемый)
Современный и правильный подход — отменить сам запрос, а не просто игнорировать ответ. Это экономит ресурсы сервера и клиента.
import React, { useState, useEffect } from 'react';
const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
setLoading(true);
fetch(`/api/users/${userId}`, { signal })
.then(res => {
if (!res.ok) throw new Error('Network error');
return res.json();
})
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
if (err.name !== 'AbortError') {
console.error('Fetch error:', err);
setLoading(false);
}
});
// Cleanup: отменяем запрос
return () => controller.abort();
}, [userId]);
if (loading) return <div>Загрузка...</div>;
return <div>{user?.name}</div>;
};
Почему это круто? Запрос реально отменяется, сервер не тратит ресурсы, а клиент не получает лишних данных. Идеально для оптимизации производительности.
Способ 3: Библиотека react-use или кастомный хук
Можно не изобретать велосипед, а взять готовый хук usePromise из react-use или написать свой универсальный хук.
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 setSafeState = useCallback((newState: React.SetStateAction<T>) => {
if (isMountedRef.current) {
setState(newState);
}
}, []);
return [state, setSafeState];
}
// Использование:
const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
const [user, setUser] = useSafeState<User | null>(null);
// ... остальной код
};
Оптимизация производительности: почему это важно?
Ошибка Can't perform a React state update on an unmounted component — это не просто «кривой» warning. Это индикатор утечки памяти. Если ваш сайт работает долго, пользователь переключает вкладки, а вы не отписываетесь от подписок (таймеры, WebSocket, fetch), память будет расти. В мобильных браузерах это может привести к зависанию или вылету приложения.
Вот чек-лист для профилактики утечек:
- Всегда возвращайте cleanup-функцию из
useEffect. - Для асинхронных запросов используйте
AbortController. - Для таймеров (
setTimeout,setInterval) — очищайте их в cleanup. - Для WebSocket и EventSource — закрывайте соединение.
- Для подписок на Redux или MobX — отписывайтесь.
Типичные сценарии, где ловится ошибка
Ошибка часто всплывает в таких ситуациях:
Блоги
Рекомендаций по книгам
Читайте также
Оптимизация производительности TypeScript приложений: советы и примеры