React 19.2 Activity Component: Keeping Unmounted Trees Alive for Faster Tab Switching
React 19.2's Activity component preserves unmounted component state without CSS hacks. Learn when to use it for forms, video players, and dashboards—and when display:none still wins.
The Tab Switching Problem Every React Developer Faces
Most state persistence problems in React stem from the framework's unmount behavior. When developers build tab systems, multi-step forms, or dashboard layouts, they typically use conditional rendering. The active tab renders, the inactive tabs unmount completely. Users switch back to a previous tab and discover their form data vanished, their video player reset to 0:00, or their carefully filtered table reverted to defaults.
Teams work around this with external state managers, CSS display: none tricks, or lifting state into parent components. These patterns work but introduce overhead. React 19.2's Activity component solves this at the framework level by preserving the component tree and state while the component is inactive. The tree stays mounted but paused—no re-renders, no effect cleanup, no data loss.
This distinction is critical. Activity doesn't hide components with CSS. It keeps the fiber tree alive in React's reconciler but skips work until the component becomes active again. The performance characteristics differ significantly from both conditional rendering and visibility toggling.
What Is the Activity Component and How Does It Work?
The Activity component wraps child components and controls their lifecycle based on a boolean mode prop. When mode="visible", the children render and behave normally—effects run, updates process, event handlers fire. When mode="hidden", React keeps the component tree mounted but pauses all work.
import { Activity } from 'react';
function TabPanel({ isActive, children }) {
return (
<Activity mode={isActive ? 'visible' : 'hidden'}>
{children}
</Activity>
);
}The paused state differs from display: none in three ways. First, React stops processing updates entirely—state changes queue but don't trigger re-renders. Second, effects don't clean up when transitioning to hidden mode. useEffect cleanup functions only run when the component actually unmounts, not when Activity hides it. Third, the component tree remains in the reconciler, so switching back to visible mode is instantaneous.
flowchart TD
A[Component renders with Activity]
B{mode='visible'?}
C[Normal lifecycle: effects run, updates process]
D[Paused lifecycle: tree mounted, work deferred]
E[User switches tab]
F[Instant activation: no remount, no data loss]
A --> B
B -->|Yes| C
B -->|No| D
D --> E
E --> F
F --> C
classDef userAction fill:#142544,stroke:#7c9cf0,color:#eaf2ff
classDef framework fill:#0b3b2e,stroke:#34d399,color:#d1fae5
class E userAction
class A,B,F framework
This matters because the pause-resume pattern eliminates the entire category of bugs where components lose internal state on tab switches. Video players remember playback position, forms retain validation state, and tables preserve filter selections—all without lifting state or adding external dependencies.
Building Your First Tab System With Activity
The simplest Activity implementation handles tab switching in a settings panel or wizard interface. The pattern requires three elements: tab buttons, content components wrapped in Activity, and local state to track the active tab.
import { Activity } from 'react';
import { useState } from 'react';
function SettingsPanel() {
const [activeTab, setActiveTab] = useState('profile');
return (
<div>
<nav>
<button onClick={() => setActiveTab('profile')}>
Profile
</button>
<button onClick={() => setActiveTab('notifications')}>
Notifications
</button>
<button onClick={() => setActiveTab('security')}>
Security
</button>
</nav>
<Activity mode={activeTab === 'profile' ? 'visible' : 'hidden'}>
<ProfileSettings />
</Activity>
<Activity mode={activeTab === 'notifications' ? 'visible' : 'hidden'}>
<NotificationSettings />
</Activity>
<Activity mode={activeTab === 'security' ? 'visible' : 'hidden'}>
<SecuritySettings />
</Activity>
</div>
);
}
function ProfileSettings() {
const [name, setName] = useState('');
// This state persists when tab switches away
return (
<form>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Display name"
/>
</form>
);
}
Each tab component manages its own state. When users switch from Profile to Notifications, ProfileSettings enters hidden mode but retains the name state value. Switch back and the input field shows exactly what the user typed before. No form library required, no parent state hoisting, no context providers.
The failure mode here is subtle but expensive. Without Activity, developers typically solve this by lifting state into SettingsPanel and passing it down as props. That works until the tab components grow complex with nested forms, validation logic, and side effects. The parent becomes a massive state orchestrator, and prop drilling turns into a maintenance nightmare.
Activity vs Display None vs Conditional Rendering: A Performance Comparison
Teams reach for three patterns when building tab systems: conditional rendering, CSS visibility, and now Activity. The tradeoffs matter for production applications handling complex UIs.
flowchart LR
subgraph ConditionalRendering["Conditional: {active && <Tab/>}"]
CR1[Mount on activation]
CR2[Unmount on deactivation]
CR3[Lose all state]
CR4[Re-run effects]
CR1 --> CR2 --> CR3 --> CR4
style CR3 stroke:#ef4444,fill:#450a0a,color:#fca5a5
end
subgraph ActivityComponent["Activity: mode=visible/hidden"]
AC1[Mount once]
AC2[Pause on deactivation]
AC3[Preserve all state]
AC4[Defer updates]
AC1 --> AC2 --> AC3 --> AC4
end
Conditional rendering delivers the smallest bundle size and lowest memory footprint. The inactive tab doesn't exist in the DOM or React tree. This works perfectly for simple content that doesn't maintain state—static text, read-only displays, or components that fetch fresh data on every mount. The cost appears when users switch tabs frequently. Each activation triggers a full component lifecycle: constructor, effects, async data fetching, derived state calculations. For a video player or data table, that overhead compounds quickly.
CSS display: none solves the state problem by keeping all tabs mounted but hidden. React continues processing updates for invisible components. If a hidden tab contains a live feed or auto-updating chart, those updates still trigger re-renders even though users can't see them. The memory overhead grows with the number of tabs, and performance degrades when multiple hidden tabs run expensive operations simultaneously.
Activity combines the benefits of both patterns. Components mount once and preserve state like CSS visibility, but React skips work for hidden tabs like conditional rendering. The implementation pauses the fiber tree instead of removing it, so memory usage stays closer to CSS visibility than conditional rendering. The key advantage emerges in applications with dozens of tabs or deeply nested component trees—switching tabs becomes instant because React doesn't rebuild the tree.
In other words, Activity optimizes for user experience over raw technical metrics. The memory footprint exceeds conditional rendering but the perceived performance wins. Users notice when a tab takes 200ms to remount. They don't notice an extra 50KB of retained state.
Real-World Use Cases: Forms, Video Players, and Dashboards
The practical applications for Activity cluster around three categories: complex forms with validation, media players with playback state, and data-heavy dashboards with filters.
flowchart TD
A[User interacts with form]
B[Activity: mode=visible]
C[Form state: values, errors, touched fields]
D[User switches to different tab]
E[Activity: mode=hidden]
F[State preserved: validation, draft content]
G[User returns to form]
H[Instant resume: no re-validation, no data loss]
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
G --> H
H --> B
classDef userAction fill:#142544,stroke:#7c9cf0,color:#eaf2ff
classDef dataStore fill:#3a2f0b,stroke:#fbbf24,color:#fef3c7
class A,D,G userAction
class C,F dataStore
Multi-step forms demonstrate the pattern's value immediately. Consider an onboarding wizard with five steps, each containing multiple fields and async validation. Without Activity, developers either lift all form state to the wizard component (creating a massive state object) or accept that users lose progress when clicking "back" to review earlier steps. Activity eliminates both problems. Each step component manages its own form state, validation runs once per step, and users can navigate freely without data loss.

Video players and audio interfaces need Activity for playback position and quality settings. When users switch tabs in a video tutorial application, the player component enters hidden mode but retains the current timestamp, playback speed, and subtitle preferences. The video element itself can stay in the DOM but paused, avoiding the expensive re-initialization that happens with conditional rendering. This pattern extends to any media-heavy application—podcast players, image galleries with zoom state, or audio workstations with timeline positions.
Dashboard applications with heavy data transformations benefit from Activity's deferred updates. A financial dashboard might show ten different chart views, each processing the same dataset with different aggregations. Without Activity, hiding a chart with CSS means React still processes updates when the underlying data changes. With Activity, those hidden charts skip re-renders entirely until users switch back to them. The data stays cached in component state, the charts remain mounted, but computation only happens when visible.
The implication here is that Activity shifts the optimization strategy. Instead of memoizing expensive computations or debouncing updates across all tabs, developers can let hidden tabs stay idle. The complexity reduction matters as much as the performance gain.
How Activity Pauses Effects and Defers Updates
The mechanism behind Activity's pause behavior differs from simple conditional logic. React tracks each fiber's activity state and skips reconciliation work for hidden subtrees. Effects behave differently than they do with unmounting.
flowchart TD
A[Component with useEffect]
B{Activity mode?}
C[visible: effect runs normally]
D[hidden: effect cleanup deferred]
E[State update queued]
F{Activity mode?}
G[visible: update processes immediately]
H[hidden: update batched until visible]
A --> B
B -->|visible| C
B -->|hidden| D
D --> E
E --> F
F -->|visible| G
F -->|hidden| H
H -->|mode changes| G
classDef framework fill:#0b3b2e,stroke:#34d399,color:#d1fae5
class B,F,G,H framework
When a component transitions from visible to hidden, React does not call effect cleanup functions. Consider a component with a WebSocket connection managed by useEffect. In normal unmounting, the cleanup function closes the socket. With Activity's hidden mode, the cleanup doesn't run. The socket stays open, the subscription remains active, but React stops processing incoming messages until the component becomes visible again.
This behavior requires careful handling for resources that consume bandwidth or processing power even when inactive. A component that subscribes to a real-time data feed should probably pause the subscription manually when entering hidden mode, rather than relying on Activity alone:
function LiveFeed() {
const [data, setData] = useState([]);
const [isActive, setIsActive] = useState(true);
useEffect(() => {
if (!isActive) return;
const subscription = dataSource.subscribe(setData);
return () => subscription.unsubscribe();
}, [isActive]);
return <Activity mode={isActive ? 'visible' : 'hidden'}>
<FeedDisplay data={data} />
</Activity>;
}State updates queued while a component is hidden batch together and process when the component becomes visible. This prevents wasted re-renders but can create a perception lag if many updates accumulate. For most UIs this batching improves performance. For components that must stay synchronized with external state even when hidden—like a chat application that shows unread counts—developers need additional patterns. Activity works best when hidden components can legitimately pause their work.
The distinction between Activity's pause and traditional unmount cleanup matters for debugging. Effect cleanup bugs often manifest as memory leaks or stale subscriptions. With Activity, those bugs might stay hidden longer because cleanup never runs until actual unmount. Teams adopting Activity need to audit effects that assume cleanup runs on every deactivation.
Production Patterns: When NOT to Use Activity
Activity solves specific problems exceptionally well, but several scenarios call for different approaches. The pattern breaks down when components must stay synchronized with rapidly changing external state, when memory constraints dominate performance concerns, or when SEO requires server-rendered content for all tabs.
flowchart TD
A{Does component need real-time sync when hidden?}
B{Is memory footprint critical?}
C{Is content SEO-critical?}
D[Use Activity]
E[Use conditional rendering + external state]
F[Use conditional rendering + code splitting]
G[Server-render all tabs + CSS visibility]
A -->|No| B
A -->|Yes| E
B -->|No| C
B -->|Yes| F
C -->|No| D
C -->|Yes| G
classDef userAction fill:#142544,stroke:#7c9cf0,color:#eaf2ff
class D,E,F,G userAction
Real-time dashboards with critical alerts demonstrate the first limitation. A monitoring dashboard showing server health across multiple tabs needs to process updates even when users view a different tab. If the CPU usage tab is hidden but a server starts failing, the notification must appear immediately. Activity would defer those updates until the user switches back. In other words, when hidden state changes have observable side effects that users need, conditional rendering with external state management like Jotai or TanStack Query becomes the better choice.
Mobile applications and memory-constrained environments expose Activity's overhead. Each hidden component tree consumes memory proportional to its complexity. An application with 20 tabs, each containing large data tables or image galleries, might exhaust available memory if all tabs stay mounted. Conditional rendering with code splitting delivers better memory characteristics in these scenarios. Load tab components on demand, let them unmount when inactive, and accept the re-initialization cost as a tradeoff for lower baseline memory usage.
SEO requirements conflict with Activity's client-side nature. Search engines need content in the initial HTML payload. A marketing site with tabbed product details should server-render all tabs and use CSS visibility for client-side switching. Activity adds no value here because the content must exist in the DOM regardless of user interaction.
The failure mode here is subtle but expensive. Teams that adopt Activity everywhere create applications that feel fast on modern devices but struggle on lower-end hardware or complex UIs. The pattern works best when applied selectively to components where state preservation meaningfully improves user experience—complex forms, media players, filtered data views. Not every tab system needs it.
Conclusion: Activity Component Changes the Game for State Persistence
React 19.2's Activity component eliminates an entire category of state management complexity. The pattern preserves component state across tab switches without CSS hacks, external state libraries, or prop drilling. For forms, media players, and data-heavy dashboards, Activity delivers instant tab switching and eliminates the data loss bugs that plague conditional rendering.
The key insight is that Activity optimizes for perceived performance over technical metrics. The memory footprint exceeds conditional rendering, but users experience tabs as instant because the component tree stays mounted. This tradeoff works for most web applications where user experience trumps marginal memory savings.
Production adoption requires understanding the pattern's boundaries. Activity excels when components can legitimately pause work while hidden. It struggles when hidden components must stay synchronized with real-time data or when memory constraints dominate. The implementation is straightforward—wrap content in <Activity mode={...}> and let React handle the pause-resume lifecycle.
That covers the essential patterns for React 19.2's Activity component. Apply these in production and the difference will be immediate. Avoid the common mistakes in 10 things not to do when building React apps, combine Activity with proper state management, and tab systems become a solved problem.