If you’ve ever built a tab system, modal, or multi-step form, you’ve probably faced this dilemma:
-
Conditional rendering (
{show && <Comp />}
) destroys the component when hidden → you lose state. -
Keeping everything mounted (
<Comp style={{ display: 'none' }}>
) preserves state, but effects and updates keep running in the background → wasted work.
The new <Activity />
component sits in between: it keeps state alive, but pauses effects and defers updates when hidden.
How <Activity />
Works
Think of <Activity />
as React’s pause button for components.
-
When
mode="visible"
(active):- Component runs normally
- Effects execute (e.g. API calls, event listeners)
- State updates trigger re-renders
- DOM is visible
-
When
mode="hidden"
(paused):- Effects are cleaned up (no API calls, intervals, subscriptions)
- State updates are remembered, but don’t trigger re-render immediately
- DOM is hidden, but not destroyed
- When made visible again → React flushes updates + resumes effects
Example 1: Tabs with Preserved State
Without <Activity />
(conditional rendering)
function App() {
const [tab, setTab] = useState("home");
return (
<>
<button onClick={() => setTab("home")}>Home</button>
<button onClick={() => setTab("settings")}>Settings</button>
{tab === "home" && <Home />}
{tab === "settings" && <Settings />}
</>
);
}
👉 Problem: when you switch tabs, the inactive component is destroyed. If you typed something in “Settings”, it resets every time.
With <Activity />
function App() {
const [tab, setTab] = useState("home");
return (
<>
<button onClick={() => setTab("home")}>Home</button>
<button onClick={() => setTab("settings")}>Settings</button>
<Activity mode={tab === "home" ? "visible" : "hidden"}>
<Home />
</Activity>
<Activity mode={tab === "settings" ? "visible" : "hidden"}>
<Settings />
</Activity>
</>
);
}
👉 Now:
- Switching tabs is instant (state preserved).
- Hidden tab doesn’t keep running effects.
- State updates in hidden tab are stored → when visible, UI is up to date.
Example 2: Notifications Component
function Notifications() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("🔔 Fetching notifications...");
const id = setInterval(() => {
setCount(c => c + 1);
}, 2000);
return () => clearInterval(id);
}, []);
return <p>You have {count} notifications.</p>;
}
function App() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(o => !o)}>Toggle Notifications</button>
<Activity mode={open ? "visible" : "hidden"}>
<Notifications />
</Activity>
</>
);
}
- Visible → interval runs, fetching every 2s
- Hidden → interval cleaned up (no fetch while hidden)
- Re-open → notifications count resumes with latest state
Advantages
- Preserves state (no reset on tab switch)
- Preloads modules → user sees screen instantly when switching
- Pauses background work → no wasted API calls or event listeners
- Better UX → smooth transitions without heavy remounting
Disadvantages
- Memory cost → components stay mounted in memory
- Extra complexity if overused → e.g. keeping dozens of activities hidden may bloat memory
In Simple terms, you can think of it like: as minimizing an app:
Not visible, but memory and state are kept.
CPU work is paused until you “maximize” it again.
🚀 This is just one of the new features in React 19.2. In the next article of this series, we’ll explore useEffectEvent — a new hook that helps you avoid stale closures in event handlers.
👋 Hey, I’m Preethi!
I’m that frontend engineer who gets way too excited about pixel-perfect UIs, funky CSS tricks, and the newest React toys (yup, including React 19.2 ✨). When I’m not squashing bugs, I’m probably writing about them—or building design systems that behave better than my coffee machine ☕.
If you enjoyed this read, stick around—I’ve got more React 19.2 goodness coming your way. Let’s learn, laugh, and ship cool stuff together 🚀.
Top comments (0)