A page looks fine during a quick test, but after navigating between screens a few times, every keypress fires twice, a timer speeds up, or an old API response replaces newer data. These bugs often come from an Effect that starts work without stopping it. A reliable React useEffect cleanup function example pairs each setup action with the code that reverses it: add and remove, connect and disconnect, start and stop, or request and cancel.
Cleanup is not a ritual that belongs in every Effect. It is resource ownership expressed in code. Once you understand when React runs the returned function, event listeners, intervals, subscriptions, and network requests become easier to reason about—and many supposed “lifecycle bugs” turn into straightforward setup-and-teardown problems.
Table of Contents
What React useEffect cleanup actually does
React useEffect cleanup function example: the core pattern
React useEffect cleanup function example patterns for common side effects
Prevent memory leaks in React: what cleanup can and cannot do
Dependencies, stale closures, and React Hooks lifecycle
Why cleanup runs twice in development
What I’ve learned from real usage
Things blogs don’t usually mention
Who should NOT use this
How to test a React useEffect cleanup function example
useEffect best practices checklist
Common failure modes and how to debug them
Frequently asked questions (FAQ)
A practical next step
What React useEffect cleanup actually does
useEffect lets a component synchronize with something React does not control, such as a browser event, timer, network connection, media API, or third-party widget. The function passed to useEffect performs setup. If that function returns another function, React treats the returned function as cleanup.
The most important detail is that cleanup is not limited to unmounting. React runs an Effect’s cleanup before it runs that Effect again because a dependency changed. It also runs cleanup after the component is removed. The current React useEffect reference describes the sequence as setup on mount, old cleanup followed by new setup after changed dependencies, and final cleanup on unmount.
Consider a chat component connected to roomId="general". If roomId changes to "support", staying connected to both rooms would be wrong. React first invokes the cleanup created by the "general" render, then runs setup from the "support" render. Each setup therefore owns one period of synchronization.
The React useEffect return function is a closure
The cleanup function retains the values from the render that created it. That is useful, not accidental. If an Effect connected to the general room, its cleanup still knows which connection object to close even after the component renders the support room.
This also explains why cleanup should usually operate on resources created in the same Effect. Storing every timer or connection in a shared variable makes ownership less clear and creates opportunities for one Effect cycle to stop another cycle’s resource.
Cleanup should mirror setup
A useful review question is: “What did this Effect start or register?” If it called addEventListener, cleanup should call removeEventListener. If it called setInterval, cleanup should call clearInterval. If it subscribed, cleanup should unsubscribe. If it opened a connection, cleanup should close that specific connection.
Some Effects do not establish an ongoing process and need no cleanup. Setting a non-reversible analytics marker, for example, is not made correct by inventing an unrelated return function. The goal is symmetry, not simply having a return.
React useEffect cleanup function example: the core pattern
The basic useEffect cleanup function has three parts: create the resource, use it, and return a function that releases it.
import { useEffect } from 'react';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
return <h2>Room: {roomId}</h2>;
}
On the first committed render, React connects to the selected room. When roomId changes, React disconnects the old connection before creating the new one. When ChatRoom unmounts, React disconnects the last connection.
The dependency array is part of the design, not a performance switch. With no dependency array, the Effect is eligible to run after every commit. With [], it does not re-synchronize because of changing component values. With [roomId], it re-synchronizes when roomId changes according to React’s dependency comparison.
Do not remove a dependency merely to make an Effect run less often. If setup or cleanup reads a prop, state value, or function declared inside the component, that reactive value normally belongs in the dependency array. The official exhaustive-deps guidance treats missing dependencies as a stale-closure problem. A better fix is usually to restructure the Effect, move stable values outside the component, or stabilize a callback when its identity genuinely matters.
React useEffect cleanup function example patterns for common side effects
The syntax barely changes across use cases, but correct cleanup depends on the API being controlled. The following patterns cover the cases that most often cause duplicate work or stale updates.
Remove an event listener in React
To remove event listener React code correctly, pass the same event type, the same function object, and a matching capture setting to removeEventListener. Creating a new anonymous function during cleanup does not match the function registered during setup.
import { useEffect } from 'react';
function EscapeListener({ onEscape }) {
useEffect(() => {
function handleKeyDown(event) {
if (event.key === 'Escape') {
onEscape();
}
}
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [onEscape]);
return null;
}
Defining handleKeyDown inside the Effect gives setup and cleanup access to the same reference. If onEscape changes identity, React removes the old listener and registers one that calls the new callback. That re-registration may be harmless; if it is frequent and costly, examine why the parent recreates the callback rather than hiding the dependency.
According to MDN’s removeEventListener reference, the type, listener, and capture value determine the match. Using consistent options on both calls avoids subtle mistakes, especially when capture mode is enabled.
Clear an interval in useEffect
An interval continues independently of React until it is cleared. If a component mounts repeatedly without cleanup, several intervals can update the same state or perform the same polling work.
import { useEffect, useState } from 'react';
function ElapsedSeconds() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const intervalId = window.setInterval(() => {
setSeconds(current => current + 1);
}, 1000);
return () => {
window.clearInterval(intervalId);
};
}, []);
return <p>{seconds} seconds</p>;
}
The functional state update avoids reading seconds inside the Effect, so seconds is not a dependency and the interval does not need to restart each second. The browser’s clearInterval API cancels the repeating action identified by the value returned from setInterval.
If the interval delay is a prop, include that delay in the dependency array. React will clear the interval using the old delay cycle before starting a new interval. For polling, also consider whether another request should start while the previous one is still pending; interval cleanup alone does not prevent overlapping requests.
Abort fetch in useEffect
Rapidly changing search text or route parameters can produce request races. A request for user A may finish after a request for user B and overwrite the correct result. AbortController can cancel supported asynchronous work, while an active flag can prevent stale continuation code from updating state.
import { useEffect, useState } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
let active = true;
async function loadUser() {
try {
setError(null);
const response = await fetch(
`/api/users/${encodeURIComponent(userId)}`,
{ signal: controller.signal }
);
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
const data = await response.json();
if (active) {
setUser(data);
}
} catch (requestError) {
if (
active &&
!(requestError instanceof DOMException &&
requestError.name === 'AbortError')
) {
setError(requestError);
}
}
}
loadUser();
return () => {
active = false;
controller.abort();
};
}, [userId]);
if (error) return <p role="alert">Could not load this user.</p>;
if (!user) return <p>Loading…</p>;
return <h2>{user.name}</h2>;
}
When userId changes, cleanup marks the old cycle inactive and aborts its request before the new Effect starts. MDN documents that abort() can stop fetch requests, response-body consumption, and streams. The flag remains valuable when work after fetch is not abortable or when using a client whose cancellation behavior differs.
Aborting a client request does not roll back work already accepted by a server. Do not treat cleanup as transaction cancellation for payments, writes, or other mutations. Those operations need server-side idempotency and domain-specific error handling.
For substantial applications, route loaders or a data-fetching library may handle cancellation, caching, deduplication, and stale responses more consistently than hand-written Effects. Vendor behavior changes over time, so verify the current documentation for the tool in use.
Unsubscribe from a stream or external store
Subscription APIs often return the cleanup operation directly. When they do, the Effect can return that function:
useEffect(() => {
const unsubscribe = notifications.subscribe(channelId, message => {
setMessages(current => [...current, message]);
});
return unsubscribe;
}, [channelId]);
Confirm the library’s contract. Some APIs return a subscription object with an unsubscribe() method instead. WebSockets, observers, media streams, and third-party widgets each have their own release method. Closing a WebSocket, calling disconnect() on an observer, or destroying a widget are related ideas, but they are not interchangeable APIs.
Prevent memory leaks in React: what cleanup can and cannot do
Cleanup can prevent retained listeners, unclosed connections, active timers, and needless asynchronous work. These resources may keep callbacks and the values captured by those callbacks reachable longer than intended. Even when memory does not grow dramatically, duplicate subscriptions can create incorrect behavior and unnecessary CPU or network use.
However, every late state update is not proof of a memory leak, and adding a cleanup function does not repair every performance problem. A large cache may be intentionally retained. A slow render may come from expensive computation rather than an Effect. A detached DOM node may be held by third-party code outside the component. Diagnose the resource that remains alive instead of labeling every post-unmount symptom a leak.
Cleanup is most effective when ownership is local: the component creates one resource and releases the same resource. Shared connections, application-wide caches, and singleton services usually need a provider or service-level lifetime rather than being closed by whichever child happens to unmount first.
Dependencies, stale closures, and React Hooks lifecycle
The React Hooks lifecycle is easier to understand as repeated synchronization than as three class-style events. Each committed Effect cycle starts with values from one render and ends with cleanup from that same render.
A missing dependency can leave setup connected to old data. For example, an event handler may continue reading an earlier userId because the Effect never re-ran. An unnecessary object or function dependency can cause the opposite problem: setup and cleanup repeat on every render because a new reference is created each time.
First make the dependency list truthful. Then reduce unnecessary re-runs structurally. Create an options object inside the Effect if only the Effect needs it. Move constants outside the component if they never depend on props or state. Use memoization when stable identity is part of the actual design, not as a blanket response to a lint warning.
Cleanup itself can have stale values too. If it reads a prop or function, that value participates in the same dependency analysis as setup. Suppressing the Hooks linter can conceal a connection that never switches accounts, a listener that calls old logic, or a cleanup that releases the wrong resource.
Why cleanup runs twice in development
With Strict Mode enabled, React performs an extra development-only setup and cleanup cycle before the real setup. This is a stress test for symmetry. Production does not include that extra cycle, but code should behave correctly whether the sequence is setup once or setup, cleanup, setup.
If a listener duplicates or a dialog fails during this check, removing Strict Mode or adding a “run once” ref usually hides the defect. The durable fix is to make cleanup fully undo setup. A development Network panel may show an extra fetch; cancellation or ignoring the stale result keeps it from affecting state. If duplicate development requests are expensive, a cache or framework data layer can deduplicate them.
Do not write tests that assume every Effect setup is called exactly once in all environments. Test the externally visible result and verify that active resources are balanced after rerendering or unmounting.
What I’ve learned from real usage
The most maintainable Effects are small and named by purpose. An Effect that registers a listener, opens a socket, updates analytics, fetches data, and changes the document title has several lifetimes mixed together. Splitting independent synchronization processes makes each return function obvious and lets dependencies describe the real trigger.
The second practical lesson is that correctness comes before avoiding re-subscription. Developers sometimes omit a callback dependency because they do not want to reattach a listener. That creates a fast but stale listener. Start with correct dependencies, measure whether re-registration matters, and only then choose a stable callback pattern supported by the React version and tooling in the project.
The third lesson is to test transitions, not just initial mount. Most cleanup defects appear when an identifier changes quickly, a modal opens and closes repeatedly, a route remounts, or a request resolves out of order. A component that works on first load has only passed the easiest case.
Things blogs don’t usually mention
React does not wait until unmount to clean an Effect. A dependency change can trigger cleanup while the component remains visible. Code that interprets cleanup as “the user left this screen” may therefore log false departures or reset state at the wrong time.
React expects the Effect callback to return either a cleanup function or nothing. Making the Effect callback itself async returns a Promise, not a cleanup function. Define and call an async function inside the Effect, as in the fetch example. Cleanup should release local resources synchronously where possible; do not assume React will await an async cleanup before continuing.
Cleanup may trigger callbacks of its own. Closing a socket can emit a close event, and destroying a widget can invoke library hooks. If those callbacks update state or reconnect automatically, the teardown path needs guards or an explicit “intentional close” state.
Not every component owns the resource it uses. If several components share one socket, the first unmounting child must not necessarily close it. Ownership may belong to a provider with reference counting or to an application service that lives until logout. Local cleanup is correct only when the resource lifetime is local.
Effects run after a render is committed; they do not run during server rendering. If a render is abandoned before commit, its Effect setup did not run, so there is nothing from that Effect to clean. This distinction matters when diagnosing concurrent rendering: render code must remain pure, while external setup belongs in an Effect.
Who should NOT use this
Do not add an Effect and cleanup function for values that can be calculated during rendering. If fullName is derived from firstName and lastName, calculate it directly rather than setting state in an Effect. Pure calculations have no external resource to release.
Work caused by a specific user action often belongs in that event handler. A purchase request, form submission, or file download should not be triggered indirectly because state changed and an Effect noticed. Event handlers preserve the reason the work happened and reduce accidental repeats.
An Effect that performs a one-off operation with no ongoing resource may not require cleanup. Conversely, global or shared resources may require cleanup, but not at the individual component level. Data fetching may also be better placed in a framework’s route-loading mechanism when server rendering, caching, or request deduplication matters.
If you cannot state what setup created and who owns it, adding return () => {} is not protection. Reconsider whether an Effect is needed and where the resource’s lifetime should live.
How to test a React useEffect cleanup function example
Test the lifecycle transitions that invoke cleanup. Render the component, change a dependency, and confirm the old resource was stopped before the new one became active. Then unmount and confirm the final resource was released.
For an event listener, spy on or wrap the external subscription boundary and verify that removed handlers are the handlers previously added. More importantly, dispatch an event after unmount and confirm the component’s callback no longer runs. Avoid asserting a single raw setup count if the test uses Strict Mode.
For intervals, use the fake-timer facility provided by the project’s test runner. Advance time while mounted, unmount, advance time again, and confirm no further updates or polling calls occur. Restore real timers after the test so one test does not contaminate another.
For fetch cancellation, mock a request that remains pending, unmount or change the request key, and inspect the supplied AbortSignal. Resolve requests in reverse order to confirm stale data cannot replace the newest result. Also test a genuine network failure so abort handling does not accidentally swallow all errors.
Browser developer tools help with integration checks. Repeatedly open and close the component while watching network connections, timer-driven logs, and registered listeners. Heap snapshots can help with retained objects, but behavioral duplication is often the faster signal for beginner and intermediate teams.
useEffect best practices checklist
Use an Effect only to synchronize with an external system.
Keep setup and cleanup in the same Effect.
Release the exact resource created by that Effect cycle.
Include every reactive value read by setup or cleanup.
Handle dependency changes as carefully as unmounting.
Test rapid changes, repeated mounts, cancellation, and errors.
Common failure modes and how to debug them
The classic event bug uses two anonymous functions:
window.addEventListener('resize', () => updateSize());
return () =>
window.removeEventListener('resize', () => updateSize());
Those arrows are different function objects, so removal fails. Assign one handler to a variable and use it for both calls.
The classic interval bug reads state inside an Effect with [], producing a stale value. Prefer a functional state update when the next value depends only on the previous value. If the timer genuinely depends on a prop such as delay or isRunning, include that prop and allow React to rebuild the timer correctly.
An infinite loop usually involves an Effect setting state while one of its dependencies changes on every resulting render. Log the dependency values and inspect object or function identity. Before adding memoization, ask whether the Effect is managing derived React state rather than synchronizing with an external system.
Another failure is returning cleanup from a click handler and expecting React to call it. React only recognizes the function returned from the useEffect callback as Effect cleanup. Event-handler return values do not register lifecycle work.
Finally, avoid unrelated state updates in cleanup. During a dependency change, cleanup sees values from the previous render, and during unmount the component is leaving anyway. If cleanup needs to notify an external store, make that ownership explicit; if it only tries to “reset” local UI state, it is often unnecessary or a sign that state belongs elsewhere.
Frequently asked questions (FAQ)
What is a React useEffect cleanup function example, and when does cleanup run?
A React useEffect cleanup function example returns a function from useEffect that reverses the Effect’s setup. React runs cleanup before rerunning the Effect when dependencies change and after the component unmounts. Common cleanup tasks include removing event listeners, clearing timers, disconnecting subscriptions, closing connections, and cancelling supported asynchronous requests.
How do I write a React useEffect cleanup function example correctly?
A correct React useEffect cleanup function example creates a resource inside the Effect and releases that same resource in the returned function. Keep setup and teardown together so ownership remains clear. The dependency array should include every reactive value used by either function. Avoid empty cleanup functions because they provide no protection when the Effect creates no ongoing resource.
How do I remove an event listener in React using useEffect cleanup?
To remove an event listener in React, cleanup must use the same event type, handler reference, and capture setting used during registration. Define the handler inside the Effect, add it with addEventListener, and remove it in the returned function. Creating a new anonymous function during removal will not match the original listener, leaving it active after rerenders or unmounting.
How do I clear an interval in useEffect without creating stale state?
To clear an interval in useEffect, save the identifier returned by setInterval and pass it to clearInterval during cleanup. When the interval updates state based on its previous value, use a functional state update to avoid stale closures. If the delay or activation condition comes from props, include those values as dependencies so React can rebuild the timer correctly.
Should I abort fetch in useEffect when a component unmounts?
You should abort fetch in useEffect when an outdated or unnecessary request can be cancelled safely. Create an AbortController, provide its signal to fetch, and call abort() during cleanup. Also guard against stale responses when later processing cannot be cancelled. Remember that client-side cancellation does not reverse server-side writes, payments, or other operations already accepted by the server.
Why does my React useEffect cleanup function example run twice?
A React useEffect cleanup function example may appear to run twice during development because Strict Mode performs an extra setup-and-cleanup cycle. This checks whether teardown fully reverses setup. It is usually better to fix duplicate listeners, timers, or connections than suppress the cycle with a ref. Production behaviour differs, but the Effect should remain correct under repeated setup and cleanup.
Can a useEffect cleanup function be asynchronous?
A useEffect cleanup function should normally release resources synchronously because React does not wait for an asynchronous cleanup to finish before continuing. The Effect callback itself should not be declared async, since that returns a Promise instead of a cleanup function. Start asynchronous work inside the Effect, then use synchronous cleanup to cancel it, mark it inactive, or disconnect its associated resource.
Is manual React useEffect cleanup still the best approach in 2026?
Manual React useEffect cleanup remains appropriate for local event listeners, timers, observers, and component-owned connections in 2026. For application-wide data fetching, route loaders or dedicated data tools may better manage caching, deduplication, cancellation, and stale responses. The better approach depends on project scale, rendering strategy, team experience, bundle constraints, and the behaviour supported by the chosen framework or library.
Can React useEffect cleanup prevent every memory leak in React?
React useEffect cleanup can prevent memory leaks caused by retained listeners, active timers, subscriptions, observers, or unclosed connections, but it cannot fix every performance problem. Large caches, third-party code, expensive renders, and intentionally shared services need separate diagnosis. Identify which resource remains active and who owns it before assuming that adding a return function will resolve the issue.
How should I test a React useEffect cleanup function example in 2026?
Test a React useEffect cleanup function example by mounting the component, changing dependencies, and confirming the old resource stops before the new one starts. Then unmount and verify the final listener, timer, subscription, connection, or request is released. Include rapid dependency changes, reversed request completion, and development Strict Mode so tests cover realistic lifecycle transitions rather than only the initial render.
A practical next step
Treat every Effect as a small contract: start synchronization for the current render, then completely stop that synchronization when React asks. Use the returned function for real resources—listeners, timers, subscriptions, connections, observers, and cancellable requests—not as boilerplate.
When reviewing existing code, begin with Effects that have addEventListener, setInterval, subscription methods, socket creation, or fetch calls. Verify the matching release operation, make the dependency list truthful, and test both a dependency change and an unmount. If no external system exists, removing the Effect may be the cleanest cleanup of all.




