Skip to content

React Activity: The Secret to Instant UI Transitions

03/12/2025 4 min read

Discover how React's new Activity component revolutionizes conditional rendering. Learn to preserve state, boost performance, and create instant UI transitions in this deep dive.

React Activity component demonstration showing instant UI transitions with preserved state

React forces a choice between two bad options when you conditionally render components.

You’re building a tabbed interface. A user fills out a form in Tab A, switches to Tab B, then comes back to Tab A.

  • Conditional rendering ({ activeTab === 'A' && <TabA /> }) unmounts the component. The form state is gone.
  • CSS hiding (<div style={{ display: activeTab === 'A' ? 'block' : 'none' }}><TabA /></div>) keeps the component mounted. State is preserved, but React keeps rendering it. If Tab A is heavy, your app slows down even when the user is on Tab B.

For years, the workarounds were global state stores, keep-alive libraries, or accepting the performance hit.

React Activity, part of the React 19.2+ updates, solves this. <Activity> preserves state like CSS hiding while cleaning up effects like unmounting.

What is <Activity>?

The <Activity> component is a primitive that tells React: “This tree is currently not visible, but keep it alive.”

When you wrap a component in <Activity mode="hidden">:

  1. State is Preserved: React holds onto the component’s state (useState, useReducer, etc.).
  2. DOM is Hidden: React applies display: none to the DOM nodes.
  3. Effects are Unmounted: Unlike simple CSS hiding, React does clean up useEffect layout effects. This frees up resources (like subscriptions or timers) while the component is hidden.
  4. Updates are Deprioritized: React knows this content isn’t visible, so it lowers the priority of any updates happening inside it (e.g., if data changes in the background).

How It Works

Let’s look at the API. It accepts a single primary prop: mode.

The Old Way (State Loss)

function App() {
  const [tab, setTab] = useState("home");

  return (
    <div>
      <button onClick={() => setTab("home")}>Home</button>
      <button onClick={() => setTab("profile")}>Profile</button>

      {/* Profile unmounts when you switch to Home. State is lost. */}
      {tab === "home" && <Home />}
      {tab === "profile" && <Profile />}
    </div>
  );
}

The New Way (Instant Transitions)

import { Activity } from "react";

function App() {
  const [tab, setTab] = useState("home");

  return (
    <div>
      <div className="tabs">
        <button onClick={() => setTab("home")}>Home</button>
        <button onClick={() => setTab("profile")}>Profile</button>
      </div>

      <div className="content">
        {/* Both components stay "mounted" but one is hidden */}
        <Activity mode={tab === "home" ? "visible" : "hidden"}>
          <Home />
        </Activity>

        <Activity mode={tab === "profile" ? "visible" : "hidden"}>
          <Profile />
        </Activity>
      </div>
    </div>
  );
}

In this example, if you type into an input in <Profile />, switch to Home, and come back, your text is still there.

Detailed Behavior

1. Effect Lookup

The most useful behavior of <Activity> is how it handles Effects.

  • Visible -> Hidden: Effects are cleaned up (unmounted).
  • Hidden -> Visible: Effects are re-run (mounted).

This is critical for performance. If your Profile component has a WebSocket connection in a useEffect, you might not want that keeping the connection open when the user is on the Home tab. <Activity> closes it automatically.

2. De-prioritized Rendering

If the Profile component is hidden, but some global data changes that would cause it to re-render, React will mark that update as low priority. It will finish rendering the visible Home tab first, and only update Profile when the main thread has idle time.

Use Cases

1. Instant Modals

Pre-render a heavy modal in the background with mode="hidden". When the user clicks “Open,” switch to mode="visible". It appears instantly because the DOM is already there, just hidden.

2. Virtualized Lists

Keep items that just scrolled off-screen “warm” for a few seconds. If the user scrolls back up, they reappear instantly without re-rendering from scratch.

3. Multi-step Forms

Preserve the state of previous steps without lifting all that state up to a massive parent context or Redux store.

Caveats & “Gotchas”

  • Text Nodes: <Activity> needs a stable DOM element to apply display: none to.
  • Tree Size: Even if updates are deprioritized, the component is in memory. Don’t wrap your entire application in 50 hidden Activity providers.
  • Focus Management: When a component becomes hidden, you might need to ensure focus moves to a visible element if the focus was inside the hidden tree.

Conclusion

React Activity removes the trade-off. You get state persistence without the rendering overhead: no state stores, no third-party libraries.