Introduction

The way React docs describes <Activity /> as a Component that let’s you hide and restore the UI and internal state of it’s children.

Let’s Understand what it means.

The Activity component has two modes visible and hidden.

  1. When the mode is visible, it creates the Effects and renders the children.
  2. When the mode is hidden, it visually hides the children using display: none and also destroys all the Effects
  3. Even when the component is hidden the Children still re-render based on props
  4. If a component starts with hidden mode, it will not create any Effects until it becomes visible, but the component is in the Browser DOM with display: none. Which means the component is rendered without calling any effects

Let’s see an Example

import { Activity, useEffect, useLayoutEffect, useState } from 'react';
import { Button } from '@/components/ui/button';
 
const ComponentInsideActivity = ({
  parentCounter,
}: {
  parentCounter: number;
}) => {
  const [counter, setCounter] = useState(0);
 
  useEffect(() => {
    console.log('Mount Effect');
    // setCounter((prev) => ++prev);
    let timerRef = setInterval(() => {
      setCounter((prev) => ++prev);
    }, 1000);
    return () => {
      clearInterval(timerRef);
      console.log('UnMount Effect');
    };
  }, []);
 
  useLayoutEffect(() => {
    console.log('Mount LayoutEffect');
    return () => {
      console.log('UnMount LayoutEffect');
    };
  }, []);
  return (
    <div>
      <p>inside activity</p>
      <p>Parent Counter: ${parentCounter}</p>
      <p>Count: ${counter}</p>
    </div>
  );
};
 
export const BasicActivity = () => {
  const [isVisible, setIsVisible] = useState(false);
  const [counter, setCounter] = useState(0);
 
  useEffect(() => {
    let timerRef = setInterval(() => {
      setCounter((prev) => ++prev);
    }, 1000);
    return () => {
      clearInterval(timerRef);
    };
  }, []);
 
  return (
    <div>
      <p>Basic Activity</p>
      <Activity mode={isVisible ? 'visible' : 'hidden'}>
        <ComponentInsideActivity parentCounter={counter} />
      </Activity>
      <Button
        onClick={() => {
          setIsVisible((prev) => !prev);
        }}
      >
        Show Content inside activity
      </Button>
    </div>
  );
};
 

When you render BasicActivity component, you will see the following behavior

  1. Initially, the ComponentInsideActivity is not visible and no effects are created
  2. In the Browser DOM you can observe the ComponentInsideActivity is present with display: none
  3. It also updates the parentCounter every second

Check the code here

When to Use

  1. The Video Component is an ideal case for Activity Component, as rendering a video is expensive
  2. Confetti Component is another good case, as it creates a lot of DOM nodes
  3. Any component that’s expensive to render like Maps, Charts, etc
  4. Prefetching all the contents in the Tabs

The Other examples like Sidebar, Form where you can save the state is good to have, but with external state management like Zustand, Jotai, Redux, etc it’s not a big deal.

Activity Component implementation

This is what it might look like

const Activity = ({mode, children}: {mode: 'hidden' | 'visible', children: ReactChildren}) => {
    // Some code implementation to run effects only when mode is visible
    // Destroy effects when mode is hidden
    // It also runs suspense enabled features when component is hidden, like `use`
    return (
    		<div style={{display: mode === 'hidden' ? 'none' : 'block'}}>
    			{children}
    		</div>
	);
}

Some Random Thoughts

  1. React Navigation and React Native Screens with Freeze Component kind of trying to achieve similar behavior like Activity Commponent
  2. Most Drawer Components use similar technique to show and hide component with display: none like Activity Component
  3. Gorhom Bottom Sheet might benefit from Activity Component