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

Какие знаешь примеры использования cleanup у useEffect?

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

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

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

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

Cleanup функция в useEffect: примеры использования

Что такое cleanup

Cleanup функция (функция очистки) — это функция, которую ты возвращаешь из useEffect. Она вызывается когда компонент размонтируется или перед выполнением эффекта в следующий раз. Это критически важно для предотвращения утечек памяти и побочных эффектов.

useEffect(() => {
  // Эффект — выполняется при монтировании

  return () => {
    // Cleanup — выполняется при размонтировании или перед следующим эффектом
  }
}, [dependencies])

Практические примеры

1. Отписка от Event Listeners

function Window() {
  const [windowSize, setWindowSize] = React.useState({
    width: window.innerWidth,
    height: window.innerHeight
  })

  React.useEffect(() => {
    // Подписываемся на resize
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      })
    }

    window.addEventListener('resize', handleResize)

    // Cleanup: отписываемся от события
    return () => {
      window.removeEventListener('resize', handleResize)
    }
  }, [])

  return <div>Width: {windowSize.width}, Height: {windowSize.height}</div>
}

2. Отмена HTTP запросов (AbortController)

function UserProfile({ userId }) {
  const [user, setUser] = React.useState(null)
  const [loading, setLoading] = React.useState(true)
  const [error, setError] = React.useState(null)

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

    setLoading(true)

    fetch(`/api/users/${userId}`, {
      signal: abortController.signal
    })
      .then(res => res.json())
      .then(data => {
        setUser(data)
        setLoading(false)
      })
      .catch(err => {
        if (err.name !== 'AbortError') {
          setError(err)
          setLoading(false)
        }
      })

    // Cleanup: отменяем запрос если компонент размонтируется
    return () => {
      abortController.abort()
    }
  }, [userId])

  if (loading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>
  return <div>{user?.name}</div>
}

3. Отмена setTimeout

function Notification() {
  const [visible, setVisible] = React.useState(true)

  React.useEffect(() => {
    if (!visible) return

    // Скрываем уведомление через 5 секунд
    const timeoutId = setTimeout(() => {
      setVisible(false)
    }, 5000)

    // Cleanup: отменяем таймер если компонент размонтируется раньше
    return () => {
      clearTimeout(timeoutId)
    }
  }, [visible])

  return visible ? <div className="notification">Message!</div> : null
}

4. Отмена setInterval

function Timer() {
  const [seconds, setSeconds] = React.useState(0)

  React.useEffect(() => {
    // Увеличиваем счетчик каждую секунду
    const intervalId = setInterval(() => {
      setSeconds(prev => prev + 1)
    }, 1000)

    // Cleanup: останавливаем интервал
    return () => {
      clearInterval(intervalId)
    }
  }, [])

  return <div>Time: {seconds}s</div>
}

5. Отписка от WebSocket

function ChatMessages() {
  const [messages, setMessages] = React.useState([])

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

    socket.onopen = () => {
      console.log('WebSocket connected')
    }

    socket.onmessage = (event) => {
      const message = JSON.parse(event.data)
      setMessages(prev => [...prev, message])
    }

    socket.onerror = (error) => {
      console.error('WebSocket error:', error)
    }

    // Cleanup: закрываем соединение
    return () => {
      if (socket.readyState === WebSocket.OPEN) {
        socket.close()
      }
    }
  }, [])

  return (
    <div>
      {messages.map(msg => <p key={msg.id}>{msg.text}</p>)}
    </div>
  )
}

6. Отписка от подписок (observer pattern)

function LocationTracker() {
  const [location, setLocation] = React.useState(null)

  React.useEffect(() => {
    // Получаем текущую позицию
    const watchId = navigator.geolocation.watchPosition(
      (position) => {
        setLocation({
          latitude: position.coords.latitude,
          longitude: position.coords.longitude
        })
      },
      (error) => {
        console.error('Geolocation error:', error)
      }
    )

    // Cleanup: отменяем отслеживание
    return () => {
      navigator.geolocation.clearWatch(watchId)
    }
  }, [])

  return location ? (
    <div>Lat: {location.latitude}, Lon: {location.longitude}</div>
  ) : (
    <div>Waiting for location...</div>
  )
}

7. Отписка от подписок в React Context или Redux

function UserStatus() {
  const [status, setStatus] = React.useState('offline')

  React.useEffect(() => {
    // Подписываемся на изменения статуса пользователя
    const unsubscribe = userStatusStore.subscribe((newStatus) => {
      setStatus(newStatus)
    })

    // Cleanup: отписываемся от магазина
    return () => {
      unsubscribe()
    }
  }, [])

  return <div>Status: {status}</div>
}

8. Очистка DOM манипуляций

function FocusInput() {
  const inputRef = React.useRef(null)

  React.useEffect(() => {
    // Устанавливаем фокус на input
    inputRef.current?.focus()

    const handleKeyDown = (e) => {
      if (e.key === 'Escape') {
        inputRef.current?.blur()
      }
    }

    document.addEventListener('keydown', handleKeyDown)

    // Cleanup: удаляем listener и сбрасываем стили
    return () => {
      document.removeEventListener('keydown', handleKeyDown)
      inputRef.current?.blur()
    }
  }, [])

  return <input ref={inputRef} type="text" />
}

9. Отписка от Intersection Observer

function LazyImage() {
  const [isVisible, setIsVisible] = React.useState(false)
  const imageRef = React.useRef(null)

  React.useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        setIsVisible(true)
        // Перестаем наблюдать после первого пересечения
        observer.unobserve(entry.target)
      }
    })

    if (imageRef.current) {
      observer.observe(imageRef.current)
    }

    // Cleanup: отписываемся от наблюдателя
    return () => {
      observer.disconnect()
    }
  }, [])

  return (
    <img
      ref={imageRef}
      src={isVisible ? '/lazy-image.jpg' : '/placeholder.jpg'}
      alt="Lazy loaded"
    />
  )
}

10. Очистка state при выполнении нового эффекта

function AutoComplete() {
  const [query, setQuery] = React.useState('')
  const [results, setResults] = React.useState([])
  const [loading, setLoading] = React.useState(false)

  React.useEffect(() => {
    if (!query) {
      setResults([])
      return
    }

    setLoading(true)

    const timeoutId = setTimeout(() => {
      fetch(`/api/search?q=${query}`)
        .then(res => res.json())
        .then(data => setResults(data))
        .finally(() => setLoading(false))
    }, 300)

    // Cleanup: отменяем текущий таймер и результаты если query изменится
    return () => {
      clearTimeout(timeoutId)
      setResults([])
      setLoading(false)
    }
  }, [query])

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      {loading && <div>Loading...</div>}
      {results.map(item => <p key={item.id}>{item.name}</p>)}
    </div>
  )
}

Когда cleanup вызывается

useEffect(() => {
  console.log('Effect runs')
  return () => console.log('Cleanup runs')
}, [dep])

Сценарии:

  1. Компонент размонтируется — cleanup всегда вызывается
  2. Dependency изменяется — cleanup вызывается перед новым эффектом
  3. Компонент переиспользуется с новыми props — cleanup вызывается перед эффектом

Порядок выполнения

// Первый рендер:
useEffect(() => {
  console.log('1. Effect (dep=5)')
  return () => console.log('cleanup')
}, [5])

// Dependency изменился на 10:
// Вывод:
// 1. cleanup
// 2. Effect (dep=10)

// Размонтирование:
// cleanup

Best Practices

Всегда очищай ресурсы:

// Плохо: утечка памяти
React.useEffect(() => {
  window.addEventListener('resize', handleResize)
  // Забыли удалить listener
}, [])

// Хорошо: очищаем ресурс
React.useEffect(() => {
  window.addEventListener('resize', handleResize)
  return () => window.removeEventListener('resize', handleResize)
}, [])

Cleanup — ответственность за правильное завершение асинхронных операций:

  • Отмена fetch запросов
  • Очистка таймеров
  • Отписка от обозревателей
  • Закрытие соединений
  • Удаление слушателей событий

Итого

Cleanup функция в useEffect:

  • Вызывается при размонтировании компонента
  • Вызывается перед выполнением эффекта если зависимости изменились
  • Предотвращает утечки памяти
  • Отменяет незавершенные асинхронные операции
  • Критична для надежного и безопасного кода