Explore Drafts
Why I never used AbortController in my React apps — and still don’t need to

You've probably seen developers reaching for AbortController in React apps to cancel fetch requests and "prevent memory leaks." It's becoming a common suggestion — but do you actually need it?
I've seen a growing number of devs recommending AbortController
to manage async behavior in React — especially for canceling fetch requests inside useEffect
. It's popping up in tutorials, Twitter threads, and even official docs.
Here's the twist:
I've never used AbortController
in any React project.
And I've never had a reason to regret it.
In this post, I'm going to walk you through why I deliberately don't use AbortController
, what happens internally in React when an async effect cleans up, and the patterns I use instead that are simpler, clearer, and often more robust.
Understanding the problem: async
calls and cleanup in React
Here's the classic scenario people worry about:
useEffect(() => {
fetch("/api/data")
.then(res => res.json())
.then(data => setData(data));
}, []);
This works… until the component unmounts before the fetch completes.
In older React versions (pre-18), this would throw a warning:
⚠️ "Can't perform a React state update on an unmounted component."
That's where AbortController
came into fashion — it gives you a way to cancel the fetch request on cleanup, theoretically avoiding the issue.
But here's what changed…
React 18 started ignoring state updates automatically
React 18 introduced a subtle but powerful internal change:
It silently ignores state updates made to unmounted components.
So in this setup:
.then(data => setData(data))
Even if the component unmounts before this runs, React will just drop the update on the floor. No error. No warning. No crash. No need to manually track if the component is still mounted.
That alone eliminates most use cases for AbortController
in UI logic.
What React does under the hood
React uses a structure called the Fiber tree to track component instances. Every time a component renders, React creates or updates its corresponding Fiber node.
When a component unmounts:
- Its Fiber node is deleted.
- Any state updates targeting that Fiber are discarded.
In ReactFiberCommitWork.js
, the update queue system checks if a component still has a valid alternate
node. If not, it exits early:
if (finishedWork.alternate === null) {
// Component unmounted — ignore the update
return;
}
In short: if React sees that the component is gone, it simply does nothing.
This is one of the most under-appreciated upgrades in React 18, and it's why you don't need to "defend" against unmounted state updates anymore.
What AbortController actually does
AbortController
lets you manually cancel an in-progress fetch()
call:
useEffect(() => {
const controller = new AbortController();
fetch("/api/data", { signal: controller.signal })
.then(res => res.json())
.then(data => setData(data))
.catch(err => {
if (err.name !== "AbortError") {
console.error("Actual error:", err);
}
});
return () => controller.abort();
}, []);
It's useful when:
- You want to stop a request from continuing
- You're making multiple fetches in a loop or with dependencies
- You're integrating with third-party APIs that limit requests
But here's the problem:
React doesn't care whether the fetch completes or aborts — it just ignores state updates if the component is gone.
So unless you're doing something outside of UI state (like tracking memory, logging, etc.), AbortController
is usually solving a problem that no longer exists.
The case against AbortController in React apps
Adds complexity
You now have to deal with AbortController
, AbortError
, signal
, and .catch()
even if you don't need to.
Doesn't integrate with async/await cleanly
You have to manually check err.name ==="AbortError"
— that clutters your logic.
Doesn't solve UI bugs in React 18+
Because React already discards state updates, you're not preventing anything meaningful in most apps.
Encourages premature optimization
99% of frontend fetches are lightweight. Unless you're doing huge streaming payloads or infinite polling, the "benefit" of aborting is minimal.
What I use instead: Listener patterns and cleanup flags
If I ever do need to prevent unnecessary fetches or avoid updates in flight, I go with this approach:
useEffect(() => {
let shouldSetData = true;
fetch("/api/data")
.then(res => res.json())
.then(data => {
if (shouldSetData) {
setData(data);
}
});
return () => {
shouldSetData = false;
};
}, []);
It's readable, predictable, and works for any kind of async task — not just fetch
.
Better yet, I sometimes combine this with event listeners. For example, don't fetch data if the user switched tabs:
useEffect(() => {
let active = true;
const onVisibilityChange = () => {
if (document.hidden) active = false;
};
document.addEventListener("visibilitychange", onVisibilityChange);
fetch("/api/data")
.then(res => res.json())
.then(data => {
if (active) setData(data);
});
return () => {
active = false;
document.removeEventListener("visibilitychange", onVisibilityChange);
};
}, []);
I'm in control — and I didn't need AbortController
at all.
When AbortController might actually be useful
There are a few legit cases:
- You're dealing with file uploads, video streaming, or large downloads
- You want to track request cancellation in logs or metrics
- You're building a custom hook used across many components
- You're doing preloading, prefetching, or other background tasks where request cost matters
But if your only concern is "I don't want to update state after unmount" — React is already handling that.
Conclusion: You don't need AbortController for UI fetches in React
I've never used AbortController
in a React project — and after diving deep into React 18's internals, I feel even more confident in that choice.
React is smarter now. It doesn't update unmounted components.
It handles state cleanup.
It gives me all the guardrails I need — and when I want more control, I reach for cleanup flags or event listeners, not low-level browser APIs.
So before you reach for AbortController
, ask yourself:
Do I actually need to cancel this request — or am I just trying to avoid an outdated warning?
In most cases, the cleanest code is the one that trusts the runtime.