Tofiq Quadri

Frontend Engineer

Profile
React 19.2 Activity Component
tofiqquadri.com

React 19.2's Activity Component: Not All That Glitters is Gold

React 19.2 introduced the new <Activity /> component, and while it's powerful for preserving UI state while hiding components, there are some important trade-offs you should know about:


โš ๏ธ Key Drawbacks

1. Memory Cost (~2x)

Keeping components hidden means keeping them in memory. Multiple hidden activities = potential memory bloat.

2. Hidden โ‰  Unmounted

The DOM persists when hidden, meaning:

  • Video/audio elements keep playing
  • Event listeners stay active
  • Animations continue running

3. Cleanup is Critical

Your Effects MUST have proper cleanup functions, or you'll face memory leaks and unintended side effects.

4. Not for Everything

Don't reach for the Activity component for simple show/hide logic. It's designed for specific use cases like:

  • Tab systems where state preservation matters
  • Pre-rendering content users will navigate to next
  • Maintaining form state during navigation

๐Ÿ’ก Bottom Line

The Activity component is a specialized tool. Use it strategically, not universally. When you do use it, ensure your components have robust Effect cleanup.


Code Examples: Common Problems

Here's sample code demonstrating the common problems with the Activity component:

โŒ PROBLEM 1: Video/Audio keeps playing when hidden

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

function VideoPlayer() {
    return (
        <div>
            <h3>Video Player</h3>
            <video controls autoPlay>
                <source src="sample-video.mp4" type="video/mp4" />
            </video>
            <p>โŒ Problem: Video audio continues even when hidden!</p>
        </div>
    );
}

โŒ PROBLEM 2: Event listeners stay active

function ProblematicCounter() {
    const [count, setCount] = useState(0);

    useEffect(() => {
        const handleKeyPress = (e) => {
            if (e.key === 'ArrowUp') {
                setCount((c) => c + 1);
            }
        };

        window.addEventListener('keydown', handleKeyPress);

        // โŒ Missing cleanup function!
        // Event listener stays active even when hidden
    }, []);

    return (
        <div>
            <h3>Counter: {count}</h3>
            <p>Press Arrow Up (event listener stays active when hidden)</p>
        </div>
    );
}

โŒ PROBLEM 3: Timers/Intervals keep running

function ProblematicTimer() {
    const [time, setTime] = useState(0);

    useEffect(() => {
        const interval = setInterval(() => {
            setTime((t) => t + 1);
            console.log('Timer running...'); // Logs even when hidden!
        }, 1000);

        // โŒ Missing cleanup - interval keeps running
    }, []);

    return (
        <div>
            <h3>Timer: {time}s</h3>
            <p>โŒ Timer keeps running in background when hidden</p>
        </div>
    );
}

โœ… SOLUTION: Proper cleanup

function FixedTimer() {
    const [time, setTime] = useState(0);

    useEffect(() => {
        const interval = setInterval(() => {
            setTime((t) => t + 1);
            console.log('Fixed timer running...');
        }, 1000);

        // โœ… Proper cleanup function
        return () => {
            clearInterval(interval);
            console.log('Timer cleaned up!');
        };
    }, []);

    return (
        <div>
            <h3>Fixed Timer: {time}s</h3>
            <p>โœ… Timer stops when hidden</p>
        </div>
    );
}

โŒ PROBLEM 4: Multiple hidden activities = memory bloat

function HeavyComponent({ id }) {
    const [data] = useState(() => {
        // Simulating heavy data
        return new Array(100000).fill(0).map((_, i) => ({
            id: i,
            value: Math.random(),
            nested: { deep: { data: 'expensive' } }
        }));
    });

    return (
        <div>
            <h4>Heavy Component {id}</h4>
            <p>Contains {data.length} items (large memory footprint)</p>
        </div>
    );
}

Main App demonstrating all problems

function App() {
    const [activeTab, setActiveTab] = useState('tab1');

    return (
        <div style={{ padding: '20px' }}>
            <h1>React Activity Component - Problems Demo</h1>

            {/* Tab Navigation */}
            <div style={{ marginBottom: '20px' }}>
                <button onClick={() => setActiveTab('tab1')}>Tab 1 (Video)</button>
                <button onClick={() => setActiveTab('tab2')}>Tab 2 (Bad Counter)</button>
                <button onClick={() => setActiveTab('tab3')}>Tab 3 (Bad Timer)</button>
                <button onClick={() => setActiveTab('tab4')}>Tab 4 (Fixed Timer)</button>
                <button onClick={() => setActiveTab('tab5')}>Tab 5 (Memory Bloat)</button>
            </div>

            {/* Problem 1: Video keeps playing */}
            <Activity mode={activeTab === 'tab1' ? 'visible' : 'hidden'}>
                <VideoPlayer />
            </Activity>

            {/* Problem 2: Event listeners stay active */}
            <Activity mode={activeTab === 'tab2' ? 'visible' : 'hidden'}>
                <ProblematicCounter />
            </Activity>

            {/* Problem 3: Timers keep running */}
            <Activity mode={activeTab === 'tab3' ? 'visible' : 'hidden'}>
                <ProblematicTimer />
            </Activity>

            {/* Solution: Proper cleanup */}
            <Activity mode={activeTab === 'tab4' ? 'visible' : 'hidden'}>
                <FixedTimer />
            </Activity>

            {/* Problem 4: Memory bloat with multiple hidden components */}
            <Activity mode={activeTab === 'tab5' ? 'visible' : 'hidden'}>
                <div>
                    <h3>Memory Bloat Example</h3>
                    <p>All 5 heavy components below stay in memory when hidden!</p>
                    {[1, 2, 3, 4, 5].map((id) => (
                        <HeavyComponent key={id} id={id} />
                    ))}
                </div>
            </Activity>
        </div>
    );
}

โœ… Fixed Video Example with Proper Cleanup

import { useRef, useEffect } from 'react';

// โœ… FIXED: Video with proper cleanup
function FixedVideoPlayer() {
    const videoRef = useRef(null);

    useEffect(() => {
        // Cleanup function pauses video when component is hidden
        return () => {
            if (videoRef.current) {
                videoRef.current.pause();
            }
        };
    }, []);

    return (
        <div>
            <h3>Fixed Video Player</h3>
            <video ref={videoRef} controls autoPlay>
                <source src="sample-video.mp4" type="video/mp4" />
            </video>
            <p>โœ… Video pauses when hidden!</p>
        </div>
    );
}

๐Ÿงช Testing the Problems

Try this:

  1. Go to Tab 1, play the video, then switch tabs - audio continues!
  2. Go to Tab 2, press Arrow Up, switch tabs, keep pressing - counter still updates!
  3. Go to Tab 3, open console, switch tabs - timer still logs!
  4. Go to Tab 4 - this one properly stops when hidden โœ…
  5. Go to Tab 5, open DevTools Memory profiler - see the memory usage!

What's your take? Have you tried the new Activity component yet?