Activity in Modern React: Preserving UI State Without Unmounting
When building interactive user interfaces—such as multi-tab workspaces, navigation sidebars, accordion panels, or multi-step checkout wizards—developers frequently toggle parts of the component tree in and out of view.
Historically, managing hidden UI in React forced engineers to accept an uncomfortable architectural compromise:
- Unmounting the Component (
{activeTab === 'editor' && <Editor />}): When the user switches tabs, the component unmounts. Its DOM nodes are destroyed, scroll positions reset, and local component state (like unsubmitted form inputs or cursor positions) is lost. Navigating back requires re-mounting, re-fetching data, and re-rendering from scratch. - Hiding with CSS (
display: none): Keeping the component mounted preserves local state and DOM nodes, but background components continue executing active Effects (timers, WebSocket listeners, continuous polling), draining device CPU and battery.
To solve this long-standing dilemma, React introduced the <Activity> component (previously developed under the experimental <Offscreen> project) (React documentation). In this guide, we explore how <Activity> operates, how mode="hidden" coordinates state preservation with Effect pausing, and how to build instantaneous, zero-lag user experiences.
What is the <Activity> Component?
<Activity> is a built-in React component that lets you control the visibility and execution priority of parts of your component tree (React blog).
Instead of destroying component fibers when UI is hidden, <Activity> wraps children in a managed boundary governed by the mode prop:
import { Activity } from 'react';
function Dashboard({ activeView }: { activeView: 'analytics' | 'settings' }) {
return (
<div className="dashboard-container">
<Activity mode={activeView === 'analytics' ? 'visible' : 'hidden'}>
<AnalyticsDashboard />
</Activity>
<Activity mode={activeView === 'settings' ? 'visible' : 'hidden'}>
<SettingsPanel />
</Activity>
</div>
);
}
How mode="visible" vs mode="hidden" Works
The behavior of <Activity> changes fundamentally depending on the mode prop:
1. When mode="visible":
- The component children are displayed normally on screen.
- React mounts and runs all Effects (
useEffect,useLayoutEffect). - State updates and user interactions are scheduled at normal priority.
2. When mode="hidden":
- DOM Preservation: React hides the child DOM nodes by applying
display: none !important, preserving their layout structures, scroll offsets, and active DOM nodes. - State Preservation: Component
useState,useReducer, and internal state remain completely intact in memory. - Effect Deactivation: React unmounts all Effects inside the hidden subtree. Any cleanup functions run immediately—stopping background intervals, disconnecting WebSocket subscriptions, and canceling animations.
- Idle Priority Updates: If state updates occur while the component is hidden (e.g., from an external store notification), React defers rendering to low-priority background idle time, ensuring that active visible UI remains fluid at 60 FPS.
When the component is switched back to mode="visible", React re-mounts the Effects, reconnects listeners, and displays the UI instantly with zero layout shifts or lost input data.
Practical Implementation: Building an Instant Multi-Tab Workspace
Let us examine a tabbed workspace where users alternate between a Markdown editor and a data table. With <Activity>, switching tabs feels instantaneous because no DOM re-creation occurs:
'use client';
import { useState, Activity } from 'react';
type Tab = 'editor' | 'preview' | 'settings';
export function Workspace() {
const [activeTab, setActiveTab] = useState<Tab>('editor');
return (
<div className="workspace">
<nav className="tab-bar">
<button
className={activeTab === 'editor' ? 'active' : ''}
onClick={() => setActiveTab('editor')}
>
Document Editor
</button>
<button
className={activeTab === 'preview' ? 'active' : ''}
onClick={() => setActiveTab('preview')}
>
Live Preview
</button>
<button
className={activeTab === 'settings' ? 'active' : ''}
onClick={() => setActiveTab('settings')}
>
Config
</button>
</nav>
<main className="tab-viewport">
{/* State, cursor position, and scroll offsets preserved */}
<Activity mode={activeTab === 'editor' ? 'visible' : 'hidden'}>
<MarkdownEditor />
</Activity>
<Activity mode={activeTab === 'preview' ? 'visible' : 'hidden'}>
<LivePreviewRenderer />
</Activity>
<Activity mode={activeTab === 'settings' ? 'visible' : 'hidden'}>
<WorkspaceSettings />
</Activity>
</main>
</div>
);
}
The User Experience Win:
In a traditional conditional render, typing in MarkdownEditor, switching to Preview, and switching back resets the editor scroll position and triggers re-compilation. With <Activity>, the editor remains in the exact state the user left it, offering a desktop-grade feel.
Prerendering Upcoming Screens in the Background
Beyond tab preservation, <Activity> unlocks background prerendering (React Labs update).
If analytics suggest that 90% of users on step 1 of an onboarding funnel proceed to step 2, an application can prerender step 2 inside <Activity mode="hidden">. React fetches any code bundles, preloads images, and calculates layouts in the background at idle priority. When the user taps "Next", the transition is instantaneous because the component is already rendered in the DOM.
Important Architectural Considerations
- Memory Overhead: While
<Activity mode="hidden">pauses Effects and background processing, the underlying DOM nodes and JavaScript objects remain allocated in browser memory. Avoid keeping hundreds of massive components hidden indefinitely. - Effects Unmounting: Remember that Effects unmount when an Activity switches to
hidden. Ensure your Effect cleanup functions cleanly tear down any event listeners or intervals, as they will re-run when the Activity becomes visible again. - Complementing Suspense:
<Activity>works harmoniously with<Suspense>. If a hidden component suspends during background prerendering, it will not trigger a visible fallback on the active screen; React will resolve the promise quietly in the background.
Conclusion
The <Activity> component introduces a missing primitive to React’s concurrent rendering toolkit. By cleanly separating visual visibility from component lifecycle, it allows developers to maintain rich state across tabs, prerender speculative views in the background, and deliver instantaneous transitions without wasting battery or CPU cycles.