Side effects often need to be cleaned up when they're no longer needed. Timers need to be cleared, subscriptions need to be unsubscribed, and fetch requests should be cancelled. Without cleanup, you get memory leaks - your app uses more and more memory, slows down, and can even crash. In this lesson, you'll learn when and how to clean up effects properly, preventing bugs and building professional React applications. ๐งน
What Is Cleanup?
A cleanup function is code that runs to reverse or stop an effect:
React.useEffect(() => {
// Effect code runs after render
console.log('Effect: Setting up...')
// Cleanup function (optional)
return () => {
console.log('Cleanup: Tearing down...')
}
}, [])
// When cleanup runs:
// 1. Before effect re-runs (if dependencies change)
// 2. When component unmounts (removed from screen)When Does Cleanup Run?
- Before the effect re-runs - Clean up previous effect before new one
- When component unmounts - Clean up when component removed
Cleanup doesn't run on the first render, only on subsequent re-runs and unmount.
Why Cleanup Is Important
Problem: Memory Leak Without Cleanup
function Timer() {
const [count, setCount] = React.useState(0)
React.useEffect(() => {
// โ BAD - Memory leak!
setInterval(() => {
setCount(prev => prev + 1)
}, 1000)
}, [])
return <p>Count: {count}</p>
}
// Problem:
// 1. Component mounts, starts interval
// 2. Component unmounts (user navigates away)
// 3. Interval keeps running!
// 4. setCount tries to update unmounted component
// 5. Memory leak + React warningsSolution: Clear Interval on Cleanup
function Timer() {
const [count, setCount] = React.useState(0)
React.useEffect(() => {
// โ
GOOD - Cleanup prevents memory leak
const intervalId = setInterval(() => {
setCount(prev => prev + 1)
}, 1000)
// Cleanup: Clear interval when component unmounts
return () => {
clearInterval(intervalId)
console.log('Interval cleared')
}
}, [])
return <p>Count: {count}</p>
}
// Flow:
// 1. Component mounts, interval starts
// 2. Component unmounts
// 3. Cleanup runs, interval cleared
// 4. No memory leak!Common Cleanup Patterns
1. Cleaning Up Timers
// setInterval - Repeating timer
function IntervalTimer() {
const [count, setCount] = React.useState(0)
React.useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev + 1)
}, 1000)
return () => clearInterval(id) // Cleanup
}, [])
return <p>{count}</p>
}
// setTimeout - One-time timer
function DelayedMessage() {
const [message, setMessage] = React.useState('')
React.useEffect(() => {
const id = setTimeout(() => {
setMessage('Hello after 3 seconds!')
}, 3000)
return () => clearTimeout(id) // Cleanup
}, [])
return <p>{message}</p>
}
// Why cleanup timeouts?
// If component unmounts before timeout fires,
// setMessage will try to update unmounted component2. Cleaning Up Event Listeners
function MouseTracker() {
const [position, setPosition] = React.useState({ x: 0, y: 0 })
React.useEffect(() => {
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY })
}
// Add listener
window.addEventListener('mousemove', handleMouseMove)
// Cleanup: Remove listener
return () => {
window.removeEventListener('mousemove', handleMouseMove)
}
}, [])
return <p>Mouse: {position.x}, {position.y}</p>
}
// Without cleanup:
// - Listener stays attached after component unmounts
// - Multiple listeners accumulate if component remounts
// - Memory leak!3. Cleaning Up Subscriptions
function ChatRoom({ roomId }) {
const [messages, setMessages] = React.useState([])
React.useEffect(() => {
// Subscribe to chat room
const subscription = chatAPI.subscribe(roomId, (message) => {
setMessages(prev => [...prev, message])
})
// Cleanup: Unsubscribe
return () => {
subscription.unsubscribe()
console.log('Unsubscribed from room', roomId)
}
}, [roomId])
return (
<div>
{messages.map((msg, i) => <p key={i}>{msg}</p>)}
</div>
)
}
// When roomId changes:
// 1. Cleanup runs: unsubscribe from old room
// 2. Effect runs: subscribe to new room4. Aborting Fetch Requests
function UserProfile({ userId }) {
const [user, setUser] = React.useState(null)
const [isLoading, setIsLoading] = React.useState(true)
React.useEffect(() => {
// Create AbortController
const controller = new AbortController()
const fetchUser = async () => {
try {
setIsLoading(true)
const response = await fetch(
`/api/users/${userId}`,
{ signal: controller.signal } // Pass signal
)
const data = await response.json()
setUser(data)
} catch (error) {
// Ignore abort errors
if (error.name !== 'AbortError') {
console.error('Fetch failed:', error)
}
} finally {
setIsLoading(false)
}
}
fetchUser()
// Cleanup: Abort fetch
return () => {
controller.abort()
console.log('Fetch aborted for user', userId)
}
}, [userId])
if (isLoading) return <p>Loading...</p>
return <div>{user.name}</div>
}
// When userId changes:
// 1. Cleanup aborts old request
// 2. Effect starts new request
// Prevents race conditions and unnecessary state updates!5. Cleaning Up External Libraries
function Map({ center }) {
const mapRef = React.useRef(null)
React.useEffect(() => {
// Initialize external library
const map = new MapLibrary(mapRef.current, {
center: center,
zoom: 10
})
console.log('Map initialized')
// Cleanup: Destroy library instance
return () => {
map.destroy()
console.log('Map destroyed')
}
}, [center])
return <div ref={mapRef} style={{ height: '400px' }} />
}
// Examples: Map libraries, chart libraries, animation libraries
// Always check documentation for cleanup methods:
// - destroy(), remove(), dispose(), cleanup(), etc.Understanding Cleanup Timing
function Example({ count }) {
console.log('1. Component rendering, count =', count)
React.useEffect(() => {
console.log('3. Effect running, count =', count)
return () => {
console.log('2. Cleanup running, previous count =', count)
}
}, [count])
console.log('4. Render complete')
return <p>Count: {count}</p>
}
// First render (count = 0):
// "1. Component rendering, count = 0"
// "4. Render complete"
// (browser paints)
// "3. Effect running, count = 0"
// Second render (count = 1):
// "1. Component rendering, count = 1"
// "4. Render complete"
// (browser paints)
// "2. Cleanup running, previous count = 0" โ Cleanup for old effect
// "3. Effect running, count = 1" โ New effect
// Component unmounts:
// "2. Cleanup running, previous count = 1" โ Final cleanup
// Key point: Cleanup runs BEFORE next effect or on unmountCleanup Execution Order:
- Component renders with new values
- Browser paints the screen
- Cleanup function runs (if effect ran before)
- New effect function runs
The cleanup has access to the OLD values from the previous render!
Interactive Demo
๐ก Tip: Edit the code above and click "Run" to see your changes
When Is Cleanup Needed?
โ You NEED Cleanup For:
- Timers - setInterval, setTimeout
- Event listeners - addEventListener
- Subscriptions - WebSocket, Firebase, etc.
- Fetch requests - AbortController
- External libraries - Maps, charts, animations
- Browser APIs - requestAnimationFrame, IntersectionObserver
โ You DON'T Need Cleanup For:
- Regular state updates - setState is safe
- localStorage/sessionStorage - No active connection
- Simple calculations - No ongoing process
- Console.log - Already completed
๐ก Rule of Thumb
If your effect:
โข Starts something ongoing (timer, listener, subscription)
โข Creates a connection that stays open
โข Registers something that needs unregistering
Then you NEED cleanup!
If your effect just:
โข Updates state once
โข Reads/writes storage once
โข Logs something
Then cleanup is NOT needed.
Common Cleanup Mistakes
1. Forgetting to Return Cleanup Function
// โ Wrong - Memory leak!
React.useEffect(() => {
const id = setInterval(() => {
console.log('Running...')
}, 1000)
clearInterval(id) // This runs immediately, doesn't help!
}, [])
// โ
Correct - Return cleanup function
React.useEffect(() => {
const id = setInterval(() => {
console.log('Running...')
}, 1000)
return () => clearInterval(id) // Cleanup function
}, [])2. Not Saving ID for Cleanup
// โ Wrong - Can't clear interval!
React.useEffect(() => {
setInterval(() => {
console.log('Running...')
}, 1000)
return () => clearInterval(???) // What ID to clear?
}, [])
// โ
Correct - Save ID
React.useEffect(() => {
const id = setInterval(() => {
console.log('Running...')
}, 1000)
return () => clearInterval(id) // Clear the right interval
}, [])3. Using Async Function as Cleanup
// โ Wrong - Cleanup can't be async!
React.useEffect(() => {
return async () => {
await someAsyncCleanup() // This won't work!
}
}, [])
// โ
Correct - Call async function inside cleanup
React.useEffect(() => {
return () => {
someAsyncCleanup() // Fire and forget
// Or wrap in promise if you need error handling
someAsyncCleanup().catch(console.error)
}
}, [])4. Missing Dependencies Affects Cleanup
// โ Wrong - Cleanup uses stale roomId
function Chat({ roomId }) {
React.useEffect(() => {
chatAPI.subscribe(roomId)
return () => {
chatAPI.unsubscribe(roomId) // Uses current roomId, but might be stale!
}
}, []) // Missing roomId dependency!
}
// โ
Correct - Include roomId in dependencies
function Chat({ roomId }) {
React.useEffect(() => {
chatAPI.subscribe(roomId)
return () => {
chatAPI.unsubscribe(roomId) // Uses roomId from effect's closure
}
}, [roomId]) // When roomId changes, cleanup uses old value, effect uses new
}Key Takeaways
- Cleanup functions prevent memory leaks and bugs
- Return a function from useEffect to run cleanup
- Cleanup runs before effect re-runs and on unmount
- Always clear timers:
clearInterval,clearTimeout - Always remove event listeners:
removeEventListener - Always unsubscribe from subscriptions
- Abort fetch requests with
AbortController - Destroy external library instances
- Cleanup has access to old values from previous render
- Not all effects need cleanup - only ongoing processes
- Save IDs/references needed for cleanup
- Cleanup functions can't be async
- Missing cleanup causes memory leaks and React warnings
What's Next?
You now understand cleanup functions and how to prevent memory leaks! You know when cleanup is needed and how to implement it properly for different scenarios.
In the next lesson, you'll learn about Dependency Arrays:
- Understanding exactly when effects run
- Common dependency array mistakes
- Infinite loop prevention
- When to use empty array vs no array
- Optimizing effect execution
Get ready to master dependency arrays and control your effects precisely! ๐ฏ