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.
- When the mode is
visible, it creates the Effects and renders the children. - When the mode is
hidden, it visually hides the children usingdisplay: noneand also destroys all the Effects - Even when the component is
hiddenthe Children still re-render based on props - If a component starts with
hiddenmode, it will not create any Effects until it becomesvisible, but the component is in the Browser DOM withdisplay: 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
- Initially, the ComponentInsideActivity is not visible and no effects are created
- In the Browser DOM you can observe the ComponentInsideActivity is present with
display: none - It also updates the parentCounter every second
Check the code here
When to Use
- The Video Component is an ideal case for Activity Component, as rendering a video is expensive
- Confetti Component is another good case, as it creates a lot of DOM nodes
- Any component that’s expensive to render like Maps, Charts, etc
- 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
- React Navigation and React Native Screens with Freeze Component kind of trying to achieve similar behavior like Activity Commponent
- Most Drawer Components use similar technique to show and hide component with
display: nonelike Activity Component - Gorhom Bottom Sheet might benefit from Activity Component