Skip to content

Blog / React Unmount Bug

State update after unmount during navigation

Sumeet Kumar Paul2 min read
The symptom
A screen keeps working after the user has left it.
The cause
Async work started in an effect is not tied to that component's lifetime.
The fix
Invalidate ownership in cleanup and check it before every state update.

Problem

When a component starts asynchronous work and the user navigates away before it resolves, the async callback may still attempt to update state. At that point, the component no longer exists in the UI tree.

This failure usually appears only under fast navigation or slow async resolution, making it easy to miss during development and code review.

  • Async work initiated inside an effect.
  • User navigates away before async resolves.
  • Component unmounts while async is still running.
  • Async callback commits state after unmount.
page.tsx
srcapppracticestate-update-after-unmount-during-navigationpage.tsx
1useEffect(() => {
2  console.log("[BROKEN] Screen mounted → async work started");
3
4  void (async () => {
5    await delay(800);
6    console.log("[BROKEN] Async finished → updating screen");
7    setStatus("loaded");
8  })();
9
10  return () => {
11    console.log("[BROKEN] Screen unmounted");
12  };
13}, []);

The async task has no awareness of the component lifecycle. Once started, it continues executing even after the component has been removed from the UI.

Solution

Each effect execution is assigned an ownership identity. Cleanup invalidates that ownership, and async work is only allowed to commit state if its ownership is still valid.

page.tsx
srcapppracticestate-update-after-unmount-during-navigationpage.tsx
1const effectIdRef = useRef(0);
2
3useEffect(() => {
4  const effectId = ++effectIdRef.current;
5  console.log("[FIXED] Screen mounted → ownership id:", effectId);
6
7  void (async () => {
8    await delay(800);
9
10    if (effectId !== effectIdRef.current) {
11      console.log("[FIXED] Async finished → ownership no longer valid, update skipped:", effectId);
12      return;
13    }
14
15    console.log("[FIXED] Async finished → ownership valid, screen updated:", effectId);
16    setStatus("loaded");
17  })();
18
19  return () => {
20    if (effectIdRef.current === effectId) {
21      effectIdRef.current = -1;
22    }
23    console.log("[FIXED] Screen unmounted → ownership invalidated:", effectId);
24  };
25}, []);

Instead of relying on mount flags, the async work validates ownership before committing state. Cleanup explicitly invalidates intent, making teardown safe even under rapid navigation.

Check your own code

  1. 01Does every async effect have a cleanup that invalidates its work?
  2. 02Could the work be cancelled outright with an AbortController?
  3. 03Have you navigated away in the middle of a slow request?
  • react unmount bug
  • state update after unmount
  • frontend lifecycle
  • async ui failure
  • navigation teardown
View the source on GitHub ↗

Written by Sumeet Kumar Paul

Senior Frontend Engineer from India. Frontend Lab is where I reproduce real interface bugs and work through the fix.

Share article:

Need a hand?

I build frontends like this for a living.

Get in touch

More from the lab