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

Как очистить подписку при размонтировании компонента?

2.0 Middle🔥 202 комментариев
#JavaScript Core

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

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

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

Очистка подписки при размонтировании компонента

Это критически важная практика для предотвращения утечек памяти в React приложениях. Нужно правильно управлять жизненным циклом подписок и ресурсов.

1. Подписки на Event Listeners

Добавляй listener в useEffect и удаляй в cleanup функции:

import { useEffect } from 'react';

function WindowResizeComponent() {
  useEffect(() => {
    // Добавляем listener при монтировании
    const handleResize = () => {
      console.log('Window resized:', window.innerWidth);
    };

    window.addEventListener('resize', handleResize);

    // Cleanup функция - удаляет listener при размонтировании
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // пустые зависимости = эффект запускается один раз при монтировании

  return <div>Resize the window</div>;
}

2. Подписки на Observable (RxJS)

Унподписывайся от Observable:

import { useEffect } from 'react';

function ObservableComponent({ dataStream$ }) {
  useEffect(() => {
    // Подписываемся на Observable
    const subscription = dataStream$.subscribe(
      (data) => {
        console.log('Data received:', data);
      },
      (error) => {
        console.error('Error:', error);
      }
    );

    // Cleanup - отписываемся
    return () => {
      subscription.unsubscribe();
    };
  }, [dataStream$]);

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

3. Подписки на WebSocket

import { useEffect } from 'react';

function WebSocketComponent() {
  useEffect(() => {
    const ws = new WebSocket('wss://echo.websocket.org/');

    ws.onopen = () => {
      console.log('Connected');
      ws.send('Hello');
    };

    ws.onmessage = (event) => {
      console.log('Message:', event.data);
    };

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

  return <div>WebSocket</div>;
}

4. Таймеры (setTimeout, setInterval)

Очищай таймеры в cleanup функции:

import { useEffect, useState } from 'react';

function TimerComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // setTimeout
    const timeoutId = setTimeout(() => {
      console.log('Timeout fired after 5 seconds');
    }, 5000);

    // setInterval
    const intervalId = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);

    // Cleanup
    return () => {
      clearTimeout(timeoutId);
      clearInterval(intervalId);
    };
  }, []);

  return <div>Count: {count}</div>;
}

5. Абортирование fetch запросов

import { useEffect, useState } from 'react';

function FetchComponent() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // Создаём AbortController для отмены запроса
    const abortController = new AbortController();

    const fetchData = async () => {
      try {
        const response = await fetch('/api/data', {
          signal: abortController.signal // передаём сигнал
        });
        const json = await response.json();
        setData(json);
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error('Fetch error:', error);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    // Cleanup - отменяем запрос
    return () => {
      abortController.abort();
    };
  }, []);

  return <div>{loading ? 'Loading...' : data}</div>;
}

6. Подписки на Redux store

import { useEffect } from 'react';
import { useSelector } from 'react-redux';

function StoreSubscriptionComponent() {
  const data = useSelector(state => state.data);

  useEffect(() => {
    // В Redux Hook этого делать не нужно - useSelector уже управляет этим
    // Но если используешь store.subscribe() напрямую:
    const unsubscribe = store.subscribe(() => {
      console.log('Store updated');
    });

    // Cleanup
    return () => {
      unsubscribe();
    };
  }, []);

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

7. Подписки на Context

import { useContext, useEffect } from 'react';

const MyContext = React.createContext();

function ContextSubscriber() {
  const { subscribe } = useContext(MyContext);

  useEffect(() => {
    // Подписываемся
    const unsubscribe = subscribe((value) => {
      console.log('Context value:', value);
    });

    // Cleanup
    return () => {
      unsubscribe();
    };
  }, [subscribe]);

  return <div>Subscribed</div>;
}

8. Несколько очисток в одном эффекте

import { useEffect } from 'react';

function ComplexComponent() {
  useEffect(() => {
    // Listener
    const handleClick = () => {};
    document.addEventListener('click', handleClick);

    // Timer
    const timerId = setInterval(() => {}, 1000);

    // WebSocket
    const ws = new WebSocket('ws://...');

    // Cleanup - удаляй всё в обратном порядке
    return () => {
      document.removeEventListener('click', handleClick);
      clearInterval(timerId);
      ws.close();
    };
  }, []);

  return <div>Complex component</div>;
}

9. Custom Hook для управления подписками

import { useEffect } from 'react';

function useSubscription(source) {
  useEffect(() => {
    if (!source) return;

    const handleUpdate = (value) => {
      console.log('Updated:', value);
    };

    source.subscribe(handleUpdate);

    return () => {
      source.unsubscribe(handleUpdate);
    };
  }, [source]);
}

// Использование
function MyComponent({ dataSource }) {
  useSubscription(dataSource);
  return <div>Subscribed</div>;
}

Частые ошибки

// НЕПРАВИЛЬНО - утечка памяти
function BadComponent() {
  useEffect(() => {
    window.addEventListener('resize', () => {
      console.log('Resized');
    });
    // Нет cleanup функции!
  }, []);
}

// ПРАВИЛЬНО
function GoodComponent() {
  useEffect(() => {
    const handler = () => console.log('Resized');
    window.addEventListener('resize', handler);
    return () => window.removeEventListener('resize', handler);
  }, []);
}

Правила при работе с подписками

  1. Всегда удаляй listener'ы и подписки в cleanup функции
  2. Используй AbortController для fetch запросов
  3. Очищай таймеры (clearTimeout, clearInterval)
  4. Закрывай WebSocket соединения
  5. Отписывайся от Observable
  6. Проверяй mounted флаг для асинхронного кода
  7. Порядок cleanup должен быть обратным порядку подписки
Как очистить подписку при размонтировании компонента? | PrepBro