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

Как решить проблему утечек памяти в useEffect?

2.0 Middle🔥 291 комментариев
#React#Оптимизация и производительность

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

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

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

Утечки памяти в useEffect и как их предотвратить

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

Проблема 1: Активные subscription при unmount

Когда компонент размонтируется, его subscription остаётся активной и продолжает обновлять state несуществующего компонента:

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    // Подписываемся на сокет
    socket.on('user-update', (data) => {
      setUser(data); // Проблема: setUser вызывается после unmount!
    });
  }, []);
  
  return <div>{user?.name}</div>;
}

// Сценарий:
// 1. Компонент монтируется -> подписываемся на socket
// 2. Пользователь навигирует на другую страницу
// 3. Компонент размонтируется
// 4. Socket продолжает отправлять события
// 5. setUser() вызывается на несуществующем компоненте
// 6. React выдаёт предупреждение "Memory leak detected"

Решение 1: Cleanup функция (return из useEffect)

Верни функцию из useEffect — она будет вызвана при unmount:

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    const handleUpdate = (data) => {
      setUser(data);
    };
    
    socket.on('user-update', handleUpdate);
    
    // CLEANUP функция
    return () => {
      socket.off('user-update', handleUpdate);
    };
  }, []);
  
  return <div>{user?.name}</div>;
}

// Теперь при unmount слушатель удаляется правильно

Проблема 2: Таймеры и интервалы

Таймеры продолжают работать даже после unmount компонента:

function Timer() {
  const [seconds, setSeconds] = useState(0);
  
  useEffect(() => {
    // Интервал продолжит работать после unmount
    const interval = setInterval(() => {
      setSeconds(s => s + 1); // Warning при unmount
    }, 1000);
  }, []);
  
  return <div>{seconds}s</div>;
}

// Оптимизация
function Timer() {
  const [seconds, setSeconds] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);
    
    return () => {
      clearInterval(interval); // Очищаем при unmount
    };
  }, []);
  
  return <div>{seconds}s</div>;
}

Проблема 3: Fetch запросы

Запросы продолжают обрабатываться после unmount, вызывая setUser на мёртвом компоненте:

function UserData({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data); // Проблема: может выполниться после unmount
      });
  }, [userId]);
  
  return <div>{user?.name}</div>;
}

// Решение 1: AbortController
function UserData({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    const controller = new AbortController();
    
    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then(res => res.json())
      .then(data => {
        setUser(data);
      })
      .catch(err => {
        if (err.name !== 'AbortError') {
          console.error(err);
        }
      });
    
    return () => {
      controller.abort(); // Отменяем запрос при unmount
    };
  }, [userId]);
  
  return <div>{user?.name}</div>;
}

// Решение 2: Проверка наличия компонента
function UserData({ userId }) {
  const [user, setUser] = useState(null);
  const isMountedRef = useRef(true);
  
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        // Проверяем живой ли компонент
        if (isMountedRef.current) {
          setUser(data);
        }
      });
    
    return () => {
      isMountedRef.current = false;
    };
  }, [userId]);
  
  return <div>{user?.name}</div>;
}

Проблема 4: DOM слушатели

Добавленные слушатели не удаляются при unmount:

function ScrollListener() {
  useEffect(() => {
    const handleScroll = () => {
      console.log('scrolling');
    };
    
    window.addEventListener('scroll', handleScroll);
    // Проблема: слушатель остаётся в window
    // Теперь все компоненты на странице слышат этот слушатель
  }, []);
  
  return <div>Content</div>;
}

// Оптимизация
function ScrollListener() {
  useEffect(() => {
    const handleScroll = () => {
      console.log('scrolling');
    };
    
    window.addEventListener('scroll', handleScroll);
    
    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, []);
  
  return <div>Content</div>;
}

Проблема 5: Замыкания в зависимостях

Если не указать зависимость, старые функции остаются в памяти:

function DataFetcher({ url }) {
  useEffect(() => {
    fetch(url).then(/* ... */);
    // Проблема: url изменится, но effect будет использовать старый url
  }, []); // Забыл добавить url в зависимости!
}

// Правильно
function DataFetcher({ url }) {
  useEffect(() => {
    fetch(url).then(/* ... */);
  }, [url]); // Указана зависимость
}

Шаблон для безопасного useEffect

function SafeComponent() {
  useEffect(() => {
    let isMounted = true;
    const controller = new AbortController();
    let timeoutId;
    
    const setupListeners = async () => {
      try {
        // Асинхронные операции
        const data = await fetch('url', { signal: controller.signal })
          .then(r => r.json());
        
        if (isMounted) {
          // Только обновляем state если компонент ещё живой
          // setState(data);
        }
      } catch (err) {
        if (err.name !== 'AbortError' && isMounted) {
          console.error(err);
        }
      }
    };
    
    // Слушатели
    window.addEventListener('resize', () => {});
    
    // Таймеры
    timeoutId = setTimeout(() => {}, 1000);
    
    setupListeners();
    
    // CLEANUP - всё очищаем
    return () => {
      isMounted = false;
      controller.abort();
      clearTimeout(timeoutId);
      window.removeEventListener('resize', () => {});
    };
  }, []);
  
  return <div></div>;
}

Чеклист для предотвращения утечек

  • Все слушатели событий удаляются в cleanup функции
  • Все таймеры (setTimeout, setInterval) очищаются
  • Fetch запросы либо отменяются (AbortController), либо проверяется isMounted
  • Все subscription отписываются
  • Зависимости правильно указаны в массиве dependencies
  • Используется ESLint плагин react-hooks для проверки

Вывод: утечки памяти в useEffect — это классическая ошибка. Всегда возвращай cleanup функцию, очищай таймеры, отменяй запросы и удаляй слушатели событий.

Как решить проблему утечек памяти в useEffect? | PrepBro