Cleanup Functions in useEffect0%

Cleanup Functions in useEffect

Beginner12 min read•Updated: Jul 11, 2026
Study MaterialsCheatsheet

Cleanup Functions in useEffect

1. Why Cleanup Is Essential

Not all side effects are "fire-and-forget". Many side effects establish connections, timers, or event listeners that will cause serious memory leaks, duplicated callbacks, or state updates on unmounted components if not properly disassembled when the component disappears:

  • Timers: setInterval, setTimeout
  • Subscriptions: WebSockets, Firebase real-time listeners, Chat services
  • DOM Event Listeners: window.addEventListener('resize', ...)
  • Abortable Fetch Requests: AbortController

To clean up after side effects, return a cleanup function from your useEffect callback.

Output
Component Mounts:
[Setup Effect Executes] (e.g. window.addEventListener('resize', handleResize))
 
Component Updates (or Unmounts):
[Cleanup Function Executes First!] (e.g. window.removeEventListener('resize', handleResize))
[New Setup Effect Executes]

2. Example: Window Resize Listener

JSX
import React, { useState, useEffect } from 'react';
 
export default function WindowDimensions() {
const [width, setWidth] = useState(window.innerWidth);
 
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
 
// 1. Setup: Attach listener
window.addEventListener('resize', handleResize);
console.log('Event listener registered');
 
// 2. Cleanup: Return function to remove listener
return () => {
window.removeEventListener('resize', handleResize);
console.log('Event listener cleaned up');
};
}, []); // Run on mount, clean up on unmount
 
return <p>Window width: {width}px</p>;
}

3. Example: Timer with setInterval

JSX
import React, { useState, useEffect } from 'react';
 
export default function ExamTimer({ initialMinutes = 30 }) {
const [secondsLeft, setSecondsLeft] = useState(initialMinutes * 60);
 
useEffect(() => {
// 1. Setup interval
const timerId = setInterval(() => {
setSecondsLeft((prev) => (prev > 0 ? prev - 1 : 0));
}, 1000);
 
// 2. Cleanup interval
return () => clearInterval(timerId);
}, []);
 
return (
<div className="timer-badge">
Time Remaining: {Math.floor(secondsLeft / 60)}m {secondsLeft % 60}s
</div>
);
}

If you omit clearInterval(timerId), the timer will keep running in the background forever—even after the user navigates to a completely different page—leaking memory and triggering phantom state updates.

4. Canceling Network Requests with AbortController

If a user rapidly toggles between different tabs or pages, an earlier API request might finish after the component has already unmounted. Use standard browser AbortController to cancel in-flight requests:

JSX
useEffect(() => {
const controller = new AbortController();
 
async function fetchUserData() {
try {
const response = await fetch(`/api/user/${userId}`, {
signal: controller.signal
});
const data = await response.json();
setUser(data);
} catch (err) {
if (err.name !== 'AbortError') {
console.error('Fetch error:', err);
}
}
}
 
fetchUserData();
 
// Cleanup: Abort fetch if userId changes or component unmounts
return () => controller.abort();
}, [userId]);

Interactive Quiz

Practice Quiz

Question 1
How do you declare a cleanup function inside useEffect?
Question 2
What severe issue occurs if an active setInterval timer is created in useEffect without a cleanup function?
Question 3
When does React execute the cleanup function?
Question 4
Which native browser API is used to abort in-flight fetch requests inside a useEffect cleanup function?
Question 5
Why does <React.StrictMode> run effect setup and cleanup twice during development?
Next Lesson

Inline Styles and Style Objects

Continue learning with hands-on practice, examples, and exercises in the upcoming topic.

Practice Quiz

Test your understanding of this lesson with 5 questions. Each question has one correct answer.

PrevNext