Effect Cleanup: Undo Subscriptions and Pending Work
Effect Cleanup: Undo Subscriptions and Pending Work
A school page listens for browser resize events. After navigating away and back, it can accidentally leave old listeners attached if the setup has no matching cleanup.
An Effect may return a cleanup function. React runs that function before replacing the Effect after a dependency change and when the component unmounts. Cleanup reverses the resource or subscription created by that setup.
Understand
Match setup to its cleanup.
Build
Remove an event listener when its panel disappears.
Check
Explain development checks without relying on exact setup counts.
Every setup owns a matching undo operation
Setup
Attach one resize listener.
Observe
The mounted panel responds to resize.
Cleanup
Remove that exact listener when the panel unmounts.
Return cleanup; do not perform it immediately
An event listener is an external subscription. addEventListener registers a function; removeEventListener needs the matching event type, function reference and relevant options. A function declared inside the Effect belongs to that setup, so its cleanup can remove exactly that function.
Returning () => removeEventListener(...) gives React work to run later. Calling removal immediately and returning its result would end the subscription during setup. Similar pairs include starting and clearing a timer, subscribing and unsubscribing, or creating and disconnecting a connection.
Mount and unmount a resize-aware school panel
The heading is visible both before and after the panel changes. The panel initially waits for a browser resize. Resizing updates the width; Hide panel unmounts the component and removes its listener. No window property is read during server rendering.
React · JavaScript with JSX
import { useEffect, useState } from 'react';
function WidthPanel() {
const [width, setWidth] = useState(null);
useEffect(() => {
function onResize() { setWidth(window.innerWidth); }
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
return <p>{width === null ? 'Waiting for resize' : 'Width: ' + width}</p>;
}
export default function App() {
const [visible, setVisible] = useState(true);
return <main><h1>School panel lifecycle</h1>
<button onClick={() => setVisible(current => !current)}>{visible ? 'Hide panel' : 'Show panel'}</button>
{visible ? <WidthPanel /> : <p>Panel hidden</p>}</main>;
}Expected screen and behavior
Initially the panel waits for resize. Hide panel displays Panel hidden and removes the listener. Show panel creates a new panel.Follow one listener through its lifetime
React · Mount and unmount a resize-aware school panel
import { useEffect, useState } from 'react';
function WidthPanel() {
const [width, setWidth] = useState(null);
useEffect(() => {
function onResize() { setWidth(window.innerWidth); }
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
return <p>{width === null ? 'Waiting for resize' : 'Width: ' + width}</p>;
}
export default function App() {
const [visible, setVisible] = useState(true);
return <main><h1>School panel lifecycle</h1>
<button onClick={() => setVisible(current => !current)}>{visible ? 'Hide panel' : 'Show panel'}</button>
{visible ? <WidthPanel /> : <p>Panel hidden</p>}</main>;
}On the initial commit, the Effect creates onResize and registers it. That closure can call this component’s setter. When Hide panel removes WidthPanel from the tree, its cleanup uses the same onResize reference to unregister it.
Showing the panel later creates fresh state and a fresh setup. StrictMode may exercise a setup-cleanup-setup sequence during development. If cleanup mirrors setup, the useful behavior remains the same instead of accumulating extra subscriptions.
Cleanup and asynchronous cancellation are related but distinct
Clearing a timeout or interval prevents future callbacks from that timer. For a fetch operation, AbortController can request cancellation through its supported API, while an ignore flag prevents a late result from updating a component whose request is obsolete. An ignore flag does not stop network traffic by itself.
Do not report a successful save simply because a component unmounted. Cleanup handles resources, not business success. A submitted server mutation may continue or have completed already even if the client stops waiting, so transaction outcomes need a separate design.
Try the school task
React · starting example
import { useEffect, useState } from 'react';
function WidthPanel() {
const [width, setWidth] = useState(null);
useEffect(() => {
function onResize() { setWidth(window.innerWidth); }
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
return <p>{width === null ? 'Waiting for resize' : 'Width: ' + width}</p>;
}
export default function App() {
const [visible, setVisible] = useState(true);
return <main><h1>School panel lifecycle</h1>
<button onClick={() => setVisible(current => !current)}>{visible ? 'Hide panel' : 'Show panel'}</button>
{visible ? <WidthPanel /> : <p>Panel hidden</p>}</main>;
}Create a reminder panel that can be hidden before its pending reminder fires. Clear the timer in cleanup so an obsolete panel does not keep scheduling work.
Worked answer
The Effect owns a timeout and returns clearTimeout for that handle. The one-second delay is for local observation only, not a timing guarantee. Hiding the panel unmounts it; showing it starts a new reminder.
React · JavaScript with JSX
import { useEffect, useState } from 'react';
function Reminder() {
const [message, setMessage] = useState('Reminder pending');
useEffect(() => {
const timer = setTimeout(() => setMessage('Check attendance'), 1000);
return () => clearTimeout(timer);
}, []);
return <p>{message}</p>;
}
export default function App() {
const [visible, setVisible] = useState(true);
return <main><h1>Reminder cleanup</h1>
<button onClick={() => setVisible(current => !current)}>{visible ? 'Hide reminder' : 'Show reminder'}</button>
{visible ? <Reminder /> : <p>Reminder hidden</p>}</main>;
}Expected screen and behavior
The reminder begins pending. Hiding it removes the panel and clears its timer; showing it begins a new reminder.Mistakes and boundaries
Cleanup is not a catch-all error handler. Rejections from async work still need handling in the operation that owns them.
Do not mark the Effect callback async: its return would be a promise rather than the cleanup function React expects. Define an async function inside the Effect, call it, and return a synchronous cleanup function.
Do not use a ref flag solely to prevent StrictMode’s extra setup while leaving missing cleanup unfixed. That hides the lifecycle problem instead of restoring ownership.
Check your reasoning
React · Mount and unmount a resize-aware school panel
import { useEffect, useState } from 'react';
function WidthPanel() {
const [width, setWidth] = useState(null);
useEffect(() => {
function onResize() { setWidth(window.innerWidth); }
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
return <p>{width === null ? 'Waiting for resize' : 'Width: ' + width}</p>;
}
export default function App() {
const [visible, setVisible] = useState(true);
return <main><h1>School panel lifecycle</h1>
<button onClick={() => setVisible(current => !current)}>{visible ? 'Hide panel' : 'Show panel'}</button>
{visible ? <WidthPanel /> : <p>Panel hidden</p>}</main>;
}Choose the answer that follows the data and the interaction, then read the explanation for each alternative.
Carry the idea forward
Pair every subscription or scheduled resource with its matching cleanup. Then learn request loading states and late-response protection.
Read the primary reference: React: adding Effect cleanup.
Reaching the recap and completing the quizzes are separate milestones.