React 19PerformanceUXConcurrent ReactArchitecture

The <Activity /> Component: How to Build OS-Level Multitasking in React

A
Andrew Clark (Inspired)
Featured Guide 30 min read

Why does the web have amnesia?

You scroll down a Twitter feed. You click a profile. You click "Back". The feed reloads. You lose your spot. You scream.
Native apps don't do this. React apps shouldn't either.

The Problem: In React, when you unmount a component (like when switching tabs), its state is destroyed. To keep it, you have to lift state up, use global stores, or hidden CSS tricks. It's tedious.

The Solution: The <Activity> component (previously known as Offscreen) allows you to "deactivate" a part of the tree without unmounting it. It's like minimizing a window instead of closing it.

02. Meet <Activity />

import { Activity } from 'react';

function App() {
  const [mode, setMode] = useState('feed');

  return (
    <div>
      {/* 
         Instead of {mode === 'feed' && }, which destroys state,
         we use Activity to keep it alive in the background.
      */}
      <Activity mode={mode === 'feed' ? 'visible' : 'hidden'}>
         <Feed /> 
      </Activity>

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

03. How It Works: Deprioritization

When an Activity is hidden:

  • State is preserved: useState, useReducer, and DOM state (mostly) stick around.
  • Rendering stops: It stops receiving updates. It goes dormant.
  • Low Priority: React treats it as low priority. CPU cycles aren't wasted on it until it becomes visible again.

It's the mechanism that powers Concurrent Mode's ability to "yield" to more important tasks.

05. Conditional vs. Hidden vs. Activity

Method State Performance
Conditional {show && } Destroyed (Unmount) Good (DOM removed)
CSS display: none Preserved Bad (Still re-renders invisibly)
<Activity /> Preserved Perfect (No re-renders while hidden)

06. Share the Knowledge

💡

Did You Know?

The Activity component was originally named <Offscreen>, a term borrowed from browser engine terminology (like OffscreenCanvas), but was renamed to Activity to better reflect its "active/inactive" behavior.

âš¡ Interactive Playground

import React, { useState, Activity } from 'react';

// 🧪 Simulated "Activity" Polyfill for React 18
// In React 19, this would be imported from 'react' directly
const MockActivity = ({ mode, children }) => {
  // Simple CSS toggle for demo purposes
  // Real <Activity> does much more under the hood!
  return (
    <div style={{ display: mode === 'visible' ? 'block' : 'none' }}>
      {children}
    </div>
  );
};

function Feed() {
  const [scroll, setScroll] = useState(0);
  return (
    <div className="p-4 bg-gray-800 rounded h-64 overflow-auto border border-gray-700"
         onScroll={(e) => setScroll(e.target.scrollTop)}>
      <h3 className="font-bold text-cyan-400 sticky top-0 bg-gray-800 pb-2">Your Feed</h3>
      <div className="text-xs text-gray-500 mb-2">Scroll Position: {Math.floor(scroll)}px</div>
      {Array.from({length: 20}).map((_, i) => (
        <div key={i} className="p-3 mb-2 bg-gray-700 rounded hover:bg-gray-600">
           Post #{i + 1}
        </div>
      ))}
    </div>
  );
}

function Profile() {
  const [name, setName] = useState('');
  return (
    <div className="p-4 bg-gray-800 rounded h-64 border border-gray-700">
      <h3 className="font-bold text-pink-400 mb-4">Edit Profile</h3>
      <input 
        value={name}
        onChange={e => setName(e.target.value)}
        placeholder="Type your name..."
        className="w-full p-2 rounded bg-gray-900 border border-gray-600 text-white mb-4"
      />
      <p className="text-gray-400 text-sm">
        Switch tabs. I will remember what you typed!
      </p>
    </div>
  );
}

export default function App() {
  const [tab, setTab] = useState('feed');

  return (
    <div className="p-6 max-w-md mx-auto bg-gray-900 border border-gray-700 rounded-xl min-h-[400px]">
      <div className="flex gap-2 mb-6 p-1 bg-black/40 rounded-lg">
        <button 
           onClick={() => setTab('feed')}
           className={`flex-1 py-2 rounded-md font-bold transition-all ${tab === 'feed' ? 'bg-cyan-600 text-white shadow-lg' : 'text-gray-400 hover:text-white'}`}
        >
           Feed
        </button>
        <button 
           onClick={() => setTab('profile')}
           className={`flex-1 py-2 rounded-md font-bold transition-all ${tab === 'profile' ? 'bg-pink-600 text-white shadow-lg' : 'text-gray-400 hover:text-white'}`}
        >
           Profile
        </button>
      </div>

      <div className="relative">
         {/* 
            In React 19, we would use <Activity mode={...}> 
            Here we use our mock to simulate the UX.
         */}
         <MockActivity mode={tab === 'feed' ? 'visible' : 'hidden'}>
            <Feed />
         </MockActivity>

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