← Back to blog

Unlocking the Power of React 19.2: Leveraging the Activity Component for Background Rendering

March 9, 2026·9 min read
ReactReact 19.2background renderingweb performance

In the ever-evolving landscape of frontend development, React continues to lead the charge with its cutting-edge features and robust ecosystem. As a senior frontend engineer specializing in React, Next.js, and TypeScript, I am constantly exploring new ways to optimize web performance and enhance user experiences. One of the most exciting additions to React 19.2 is the <Activity> component, which brings intelligent background rendering and state preservation to the forefront of our development toolkit. In this post, we'll dive deep into this feature, exploring how it can be leveraged to improve performance and responsiveness in your React applications.

Understanding the Activity Component

Released on October 1, 2025, React 19.2 introduced <Activity> as its headline feature. The component lets you hide and restore the UI and internal state of its children without unmounting them. This is a deliberate design choice that sits between two existing approaches:

  • Conditional rendering ({condition && <Component />}) — completely destroys and recreates state and DOM on every toggle
  • CSS-only hiding (display: none) — preserves state and DOM but keeps Effects running, wasting resources

<Activity> gives you the best of both worlds: it visually hides children, cleans up Effects to free resources, yet preserves the component state and DOM in memory for instant restoration.

The API

The component is straightforward:

import { Activity } from 'react';
 
// mode defaults to 'visible'
<Activity mode="visible">
  {children}
</Activity>
 
// or hidden:
<Activity mode="hidden">
  {children}
</Activity>

The mode prop accepts two values:

  • 'visible' (default) — children render and display normally; Effects mount and run; updates are processed at normal priority
  • 'hidden' — children are hidden via display: none; Effects are cleaned up; state and DOM are preserved; updates are deferred until React has nothing else to do

Why This Matters

Consider a tabbed interface. With conditional rendering, switching tabs destroys the previous tab's state — any text typed in a form, scroll position, or fetched data is gone. With <Activity mode="hidden">, the tab is preserved in memory, and switching back is instant.

Unmounted (&&)<Activity mode="hidden">
StateDestroyedPreserved
DOMDestroyedPreserved
EffectsCleaned upCleaned up
Re-renderingDeferred (idle priority)

Practical Use Cases

1. Preserving State Across Tab Switches

The most common use case — replacing conditional rendering with <Activity> to preserve user input and component state when toggling between views:

import { Activity, useState } from 'react';
import Sidebar from './Sidebar';
 
export default function App() {
  const [isShowingSidebar, setIsShowingSidebar] = useState(true);
 
  return (
    <>
      <Activity mode={isShowingSidebar ? 'visible' : 'hidden'}>
        <Sidebar />
      </Activity>
      <main>
        <button onClick={() => setIsShowingSidebar(!isShowingSidebar)}>
          Toggle sidebar
        </button>
        <h1>Main content</h1>
      </main>
    </>
  );
}

Any state inside <Sidebar> — scroll position, open dropdowns, form values — is preserved when the sidebar is hidden and restored immediately when shown again.

2. Pre-rendering Hidden Tabs with Suspense

When combined with <Suspense>, <Activity> enables background pre-fetching of data for tabs the user hasn't navigated to yet. This only works with Suspense-enabled data sources (Relay, Next.js data fetching, React.lazy(), or use() with cached Promises):

import { Activity, Suspense, useState } from 'react';
import Home from './Home';
import Posts from './Posts';
import Contact from './Contact';
 
export default function TabContainer() {
  const [activeTab, setActiveTab] = useState<'home' | 'posts' | 'contact'>('home');
 
  return (
    <>
      <nav>
        <button onClick={() => setActiveTab('home')}>Home</button>
        <button onClick={() => setActiveTab('posts')}>Posts</button>
        <button onClick={() => setActiveTab('contact')}>Contact</button>
      </nav>
      <Suspense fallback={<p>Loading...</p>}>
        <Activity mode={activeTab === 'home' ? 'visible' : 'hidden'}>
          <Home />
        </Activity>
        <Activity mode={activeTab === 'posts' ? 'visible' : 'hidden'}>
          <Posts />
        </Activity>
        <Activity mode={activeTab === 'contact' ? 'visible' : 'hidden'}>
          <Contact />
        </Activity>
      </Suspense>
    </>
  );
}

While the user is on Home, the Posts and Contact tabs begin fetching their data in the background at idle priority. When the user clicks a tab, it often appears instantly — the data is already there.

3. Selective SSR Hydration

On the server side, <Activity> participates in selective hydration. Less critical parts of your page can hydrate at lower priority, keeping the main content interactive sooner:

export default function Page() {
  return (
    <>
      <Post />
      <Activity>
        {/* Hydrated at lower priority — doesn't block Post interactivity */}
        <Comments />
      </Activity>
    </>
  );
}

Important Caveats

Effects Are Cleaned Up on Hide

When mode switches to 'hidden', React tears down all Effects in the subtree. This means your Effects must have proper cleanup functions — the same requirement as StrictMode. When the component is made visible again, Effects re-run.

useEffect(() => {
  const subscription = subscribe(id);
  return () => subscription.unsubscribe(); // Always provide cleanup
}, [id]);

Media Elements Keep Playing

Since the DOM is preserved (not destroyed), <video> and <audio> elements continue playing when hidden. Use useLayoutEffect to pause them:

useLayoutEffect(() => {
  const video = videoRef.current;
  return () => {
    video.pause(); // Runs when Activity hides this component
  };
}, []);

useLayoutEffect is required here (not useEffect) because it's tied to the visual hide/show cycle.

Pre-rendering Requires Suspense Data Sources

Background pre-rendering only works with Suspense-enabled data sources. Data fetched inside useEffect is not detected by React's scheduler and won't pre-fetch while hidden.

Integration with ViewTransition

<Activity> integrates with the <ViewTransition> component introduced alongside it. When a hidden activity becomes visible inside a startTransition call, it automatically triggers enter/exit animations:

import { Activity, ViewTransition, startTransition } from 'react';
 
startTransition(() => {
  setActivePanel('details');
});
 
// Inside render:
<ViewTransition>
  <Activity mode={activePanel === 'details' ? 'visible' : 'hidden'}>
    <DetailsPanel />
  </Activity>
</ViewTransition>

Real-World Application: Data-Intensive Dashboard

In one of my projects, I used <Activity> to optimize a dashboard with multiple heavy data panels. Previously, switching panels destroyed and re-fetched all data on every toggle. After wrapping each panel in <Activity>:

  • Switching between panels became instant — state and DOM already exist in memory
  • Background panels pre-fetch their data at idle priority while the user works in the visible panel
  • Navigation feels native-app-fast rather than SPA-sluggish

Summary

The <Activity> component in React 19.2 fills a genuine gap that existed between full unmounting and keeping everything alive. It is purpose-built for:

  • Tab interfaces where you want to preserve user state across switches
  • Pre-rendering content the user is likely to navigate to next
  • SSR selective hydration to prioritize above-the-fold content
  • Animation integration via ViewTransition

It is not a mechanism for moving heavy JavaScript computation off the main thread — that still requires Web Workers. Its strength is in rendering priority and state preservation, enabling the kind of snappy navigation that users expect from modern web applications.

For more insights into my work and the services I offer, feel free to explore my services and my work.

Related Posts