Your React Native Fetches Are Leaking Android Quietly Solved This
What structured concurrency on Android can teach you about the async you’ve been firing and forgetting
Your React Native Fetches Are Leaking Android Quietly Solved This
What structured concurrency on Android can teach you about the async you’ve been firing and forgetting
You’ve seen this warning. You navigate away from a screen mid-request, and a few seconds later the console lights up: a state update on a component that’s no longer mounted. Maybe you’ve shrugged it off. It’s just a warning. The app didn’t crash.
But the warning is the polite version of the problem. The real version is a fetch still running for a screen that no longer exists — resolving into nothing, occasionally racing another request and winning when it shouldn’t. You fired an async operation and nobody is responsible for cleaning it up. In React Native, that’s the default, and most of us write it without thinking twice.
Android forces you to think about it. And once you see how, you start to notice how loose your own async actually is.
The warning isn’t the bug. The leaked request behind it is.
What Android Does Differently
On modern Android, async work runs inside a coroutine scope — and scopes have owners. The most common one lives on the ViewModel: viewModelScope. When you launch a coroutine there, it's bound to that scope's lifecycle.
kotlin
class FeedViewModel : ViewModel() {
private val _state = MutableStateFlow<FeedState>(FeedState.Loading)
val state = _state.asStateFlow() // what the UI observes
fun loadFeed() {
viewModelScope.launch {
val posts = repository.fetchPosts() // suspends
_state.value = FeedState.Loaded(posts)
}
}
}
Now watch what happens when the user leaves. When the ViewModel is cleared, viewModelScope is cancelled — and every coroutine inside it is cancelled with it. The in-flight network call stops. The suspended continuation never resumes. There's no stale state update, because there's no code left alive to make one.
Nobody wrote cleanup logic. There’s no teardown callback, no flag to check, no isMounted ref. The cancellation is structural — it falls out of where the work was launched. That's the whole idea behind structured concurrency: every async task has a parent, and when the parent dies, its children die with it. You can't accidentally orphan a coroutine, because launching one without a scope isn't a thing you're allowed to do.
Now Look at Your fetch
Here’s the same operation in React Native, written the way most of us write it:
tsx
function Feed() {
const [posts, setPosts] = useState([]);
useEffect(() => {
async function load() {
const data = await fetchPosts();
setPosts(data); // runs even if we've already left the screen
}
load();
}, []);
return <PostList posts={posts} />;
}
There’s no owner here. The fetchPosts() promise has no idea a component started it, and certainly no idea that component just unmounted. It runs to completion regardless, then calls setPosts into the void. The JS runtime doesn't tear down your promises when a screen leaves the tree — they keep going, because nothing told them not to.
That’s the realization worth sitting with: your async isn’t structured. Every fetch, every await, every promise you kick off from a component is fire-and-forget unless you do something about it. Android made you wire up an owner before you could even write the request. RN let you skip that step — which feels like freedom right up until the race condition.
The Plumbing Is Different, Too
It’s worth being honest about one mismatch, because the analogy isn’t perfect. A cancelled coroutine actually stops — suspending functions throw a CancellationException and unwind. A JavaScript promise has no native "stop." You can't reach into an in-flight promise and halt it. The best you can do is wire up an AbortController, signal the underlying request to abort, and ignore whatever comes back.
So Android cancels the work. RN, at best, abandons it. The mental model — every request has an owner that can kill it — is the part worth importing. The mechanism underneath is yours to approximate.
Why React Query Exists
If the fix for unstructured async sounds like a lot of bookkeeping — an AbortController per request, cleanup in every effect, a way to discard stale responses — that's because it is. And it's exactly the bookkeeping React Query was built to delete.
tsx
function Feed() {
const { data: posts = [] } = useQuery({
queryKey: ['posts'],
queryFn: ({ signal }) => fetchPosts(signal), // signal handed to you
});
return <PostList posts={posts} />;
}
That signal is the whole point. The query is tied to the component that mounted it — unmount, and the query is cancelled, the AbortSignal is passed to your fetcher automatically, stale responses are discarded, and races resolve in favor of the request that should win. You stopped writing teardown logic because the library made the lifecycle its problem.
Which is the same move Android made years ago. viewModelScope is structured concurrency baked into the framework; React Query is structured concurrency bolted onto a runtime that didn't ship with it. Different roads, same destination — async work that knows who owns it and dies when that owner does.
The Takeaway
The point of looking at Android here isn’t envy. It’s recognition. Android made the lifecycle of your async work impossible to ignore — and in doing so, it makes a RN engineer notice the thing the JS runtime quietly let them skip. Your promises don’t have owners by default. Most of the time you get away with it. The day you don’t, you’ll be staring at a race condition wondering how a screen you closed managed to overwrite the one you’re looking at.
You don’t have to adopt coroutines to benefit from this. You just have to start asking the question Android asks for you: who owns this request, and what happens to it when the screen is gone?
메타데이터
- post_id
- 39855b8d054c
- slug
- your-react-native-fetches-are-leaking-android-quietly-solved-this-39855b8d054c
- url
- https://medium.com/@pelumiogundipe905/your-react-native-fetches-are-leaking-android-quietly-solved-this-39855b8d054c
- canonical_url
- https://medium.com/@pelumiogundipe905/your-react-native-fetches-are-leaking-android-quietly-solved-this-39855b8d054c
- author_url
- https://medium.com/@pelumiogundipe905
- status
- ok
- fetched_at
- 2026-06-17 08:20:12