← Назад к вопросам

Как избежать утечек памяти при размонтировании?

2.2 Middle🔥 141 комментариев
#React

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как избежать утечек памяти при размонтировании компонента

Утечка памяти происходит, когда компонент размонтирован, но на него остаются активные ссылки, таймеры, слушатели событий или асинхронные операции. Это приводит к ненужному использованию памяти и замедлению приложения.

Основная проблема: незавершённые операции

// ❌ Утечка памяти: setInterval никогда не очищается
function Timer() {
  useEffect(() => {
    setInterval(() => {
      console.log('Tick');
    }, 1000);
  }, []);

  return <div>Timer</div>;
}

// Каждый раз, когда компонент монтируется и размонтируется,
// создаётся новый setInterval, который НИКОГДА не останавливается

Решение 1: Очистка через return в useEffect

Вернёшь функцию cleanup из useEffect, которая выполняется при размонтировании:

function Timer() {
  useEffect(() => {
    const intervalId = setInterval(() => {
      console.log('Tick');
    }, 1000);

    // ✅ Cleanup: очищает интервал при размонтировании
    return () => clearInterval(intervalId);
  }, []);

  return <div>Timer</div>;
}

Как это работает:

  • При монтировании: запускается setInterval
  • При размонтировании: выполняется cleanup функция и clearInterval
  • Интервал полностью удаляется из памяти

Решение 2: Очистка слушателей событий

// ❌ Утечка: addEventListener без removeEventListener
function Window() {
  useEffect(() => {
    window.addEventListener('resize', handleResize);
  }, []);

  return <div>Responsive</div>;
}

// ✅ Правильно: cleanup удаляет слушатель
function Window() {
  useEffect(() => {
    const handleResize = () => {
      console.log('Resized');
    };

    window.addEventListener('resize', handleResize);

    // Cleanup
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return <div>Responsive</div>;
}

Решение 3: Очистка асинхронных операций (fetch, axios)

// ❌ Утечка: fetch завершится даже после размонтирования
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(data => setUser(data)); // Ошибка: компонент может быть размонтирован!
  }, [userId]);

  return <div>{user?.name}</div>;
}

// ✅ Правильно: используй AbortController
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then(r => r.json())
      .then(data => {
        // Проверяем, что компонент ещё смонтирован
        if (!controller.signal.aborted) {
          setUser(data);
        }
      })
      .catch(error => {
        // AbortError = компонент был размонтирован
        if (error.name !== 'AbortError') {
          console.error(error);
        }
      });

    // Cleanup: отменяет запрос при размонтировании
    return () => controller.abort();
  }, [userId]);

  return <div>{user?.name}</div>;
}

Решение 4: Ref флаг для отслеживания монтирования

function DataFetcher() {
  const [data, setData] = useState(null);
  const isMountedRef = useRef(true);

  useEffect(() => {
    isMountedRef.current = true;

    const fetchData = async () => {
      const result = await api.get('/data');

      // ✅ Проверка: установим state только если компонент всё ещё смонтирован
      if (isMountedRef.current) {
        setData(result);
      }
    };

    fetchData();

    // Cleanup: отметим, что компонент размонтирован
    return () => {
      isMountedRef.current = false;
    };
  }, []);

  return <div>{data?.name}</div>;
}

Решение 5: Очистка таймеров (setTimeout, setInterval)

// ❌ Утечка
function Notification() {
  useEffect(() => {
    setTimeout(() => {
      console.log('Notification');
    }, 3000);
  }, []);

  return <div>Notification</div>;
}

// ✅ Правильно
function Notification() {
  useEffect(() => {
    const timeoutId = setTimeout(() => {
      console.log('Notification');
    }, 3000);

    return () => clearTimeout(timeoutId);
  }, []);

  return <div>Notification</div>;
}

Решение 6: Очистка подписок (Observable, WebSocket)

// ❌ Утечка: подписка никогда не отменяется
function DataStream() {
  const [data, setData] = useState(null);

  useEffect(() => {
    const subscription = dataStream$.subscribe(value => {
      setData(value);
    });
  }, []);

  return <div>{data}</div>;
}

// ✅ Правильно: отменяем подписку
function DataStream() {
  const [data, setData] = useState(null);

  useEffect(() => {
    const subscription = dataStream$.subscribe(value => {
      setData(value);
    });

    return () => subscription.unsubscribe();
  }, []);

  return <div>{data}</div>;
}

Решение 7: Очистка WebSocket соединений

function LiveChat() {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const ws = new WebSocket('wss://api.example.com/chat');

    ws.onmessage = (event) => {
      setMessages(prev => [...prev, event.data]);
    };

    // ✅ Cleanup: закрываем соединение
    return () => {
      ws.close();
    };
  }, []);

  return <div>{messages.join(', ')}</div>;
}

Чеклист для избежания утечек

В каждом useEffect с побочными эффектами:

  1. Таймеры (setTimeout, setInterval) — clearTimeout/clearInterval
  2. Слушатели событий (addEventListener) — removeEventListener
  3. Асинхронные запросы (fetch) — AbortController или флаг isMounted
  4. Подписки (RxJS) — unsubscribe()
  5. WebSocket, WebRTC — close()
  6. DOM манипуляции — очистить ссылки на DOM элементы

Пример с несколькими операциями

function ComplexComponent() {
  const [data, setData] = useState(null);
  const isMountedRef = useRef(true);

  useEffect(() => {
    isMountedRef.current = true;

    // 1. Fetch
    const controller = new AbortController();
    fetch('/api/data', { signal: controller.signal })
      .then(r => r.json())
      .then(d => {
        if (isMountedRef.current) setData(d);
      });

    // 2. Интервал
    const intervalId = setInterval(() => {
      console.log('Poll');
    }, 5000);

    // 3. Слушатель
    const handleResize = () => console.log('Resize');
    window.addEventListener('resize', handleResize);

    // ✅ Cleanup всего
    return () => {
      isMountedRef.current = false;
      controller.abort();
      clearInterval(intervalId);
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return <div>{data?.name}</div>;
}

Резюме

Утечки памяти при размонтировании происходят из-за незавершённых операций. Всегда используй cleanup функции в useEffect:

  • Таймеры → clearTimeout/clearInterval
  • События → removeEventListener
  • Запросы → AbortController
  • Подписки → unsubscribe()
  • Соединения → close()

Это критично для создания эффективного и стабильного приложения.

Как избежать утечек памяти при размонтировании? | PrepBro