<Activity>
<Activity>
lets you hide and restore the UI and internal state of its children.
<Activity mode={visibility}>
<Sidebar />
</Activity>
Reference
<Activity>
You can use Activity to hide part of your application:
<Activity mode={isShowingSidebar ? "visible" : "hidden"}>
<Sidebar />
</Activity>
When an Activity boundary becomes hidden, React will visually hide its children using the display: "none"
CSS property. It will also destroy their Effects, cleaning up any active subscriptions.
While hidden, children still receive updates, albeit at a lower priority than the rest of the content.
When the boundary becomes visible again, React will reveal the children with their previous state restored, and create their Effects.
In this way, Activity can thought of as a mechanism for rendering “background activity”. Rather than unmounting content that’s likely to become visible again, you can use Activity to maintain and restore that content’s UI and internal state.
Props
children
: The UI you intend to show and hide.- optional
mode
: Either “visible” or “hidden”. Defaults to “visible”. When “hidden”, updates to the children are deferred to lower priority. The component will not create Effects until the Activity is switched to “visible”. If a “visible” Activity switches to “hidden”, the Effects will be destroyed.
Caveats
- While hidden, the
children
of<Activity>
are visually hidden on the page. <Activity>
will unmount all Effects when switching from “visible” to “hidden” without destroying React or DOM state. This means Effects that are expected to run only once on mount will run again when switching from “hidden” to “visible”. Conceptually, “hidden” Activities are unmounted, but they are not destroyed either. We recommend using<StrictMode>
to catch any unexpected side-effects from this behavior.- When used with
<ViewTransition>
, hidden activities that reveal in a transition will activate an “enter” animation. Visible Activities hidden in a transition will activate an “exit” animation. - Parts of the UI wrapped in
<Activity mode="visible">
will hydrate at a lower priority than other content.
Usage
Restoring the state of hidden components
Typically in React, when you want to conditionally show or hide a component, you mount and unmount it:
{isShowingSidebar && (
<Sidebar />
)}
But unmounting a component destroys its internal state, which is not always what you want.
When you hide a component using an Activity boundary instead, React will “save” its state for later:
<Activity mode={isShowingSidebar ? "visible" : "hidden"}>
<Sidebar />
</Activity>
This makes it possible to restore components to their previous state.
The following example has a sidebar with an expandable section – you can press “Overview” to reveal the three subitems below it. The main app area also has a button that hides and shows the sidebar.
Try expanding the Overview section, then toggling the sidebar closed then open:
import { useState } from 'react'; import Sidebar from './Sidebar.js'; export default function App() { const [isShowingSidebar, setIsShowingSidebar] = useState(true); return ( <> {isShowingSidebar && ( <Sidebar /> )} <main> <button onClick={() => setIsShowingSidebar(!isShowingSidebar)}> Toggle sidebar </button> <h1>Main content</h1> </main> </> ); }
The Overview section always starts out collapsed. Because we unmount the sidebar when isShowingSidebar
flips to false
, all its internal state is lost.
This is a perfect use case for Activity. We can preserve the internal state of our sidebar, even when visually hiding it.
Let’s replace the conditional rendering of our sidebar with an Activity boundary:
// Before
{isShowingSidebar && (
<Sidebar />
)}
// After
<Activity mode={isShowingSidebar ? 'visible' : 'hidden'}>
<Sidebar />
</Activity>
and check out the new behavior:
import { unstable_Activity as Activity, useState } from 'react'; import Sidebar from './Sidebar.js'; export default function App() { const [isShowingSidebar, setIsShowingSidebar] = useState(true); return ( <> <Activity mode={isShowingSidebar ? 'visible' : 'hidden'}> <Sidebar /> </Activity> <main> <button onClick={() => setIsShowingSidebar(!isShowingSidebar)}> Toggle sidebar </button> <h1>Main content</h1> </main> </> ); }
Our sidebar’s internal state is now restored, without any changes to its implementation.
Restoring the DOM of hidden components
Since Activity boundaries hide their children using display: none
, their children’s DOM is also preserved when hidden. This makes them great for maintaining ephemeral state in parts of the UI that the user is likely to interact with again.
In this example, the Contact tab has a <textarea>
where the user can enter a message. If you enter some text, change to the Home tab, then change back to the Contact tab, the draft message is lost:
export default function Contact() { return ( <div> <p>Send me a message!</p> <textarea /> <p>You can find me online here:</p> <ul> <li>admin@mysite.com</li> <li>+123456789</li> </ul> </div> ); }
This is because we’re fully unmounting Contact
in App
. When the Contact tab unmounts, the <textarea>
element’s internal DOM state is lost.
If we switch to using an Activity boundary to show and hide the active tab, we can preserve the state of each tab’s DOM. Try entering text and switching tabs again, and you’ll see the draft message is no longer reset:
import { useState, unstable_Activity as Activity } from 'react'; import TabButton from './TabButton.js'; import Home from './Home.js'; import Contact from './Contact.js'; export default function App() { const [activeTab, setActiveTab] = useState('contact'); return ( <> <TabButton isActive={activeTab === 'home'} onClick={() => setActiveTab('home')} > Home </TabButton> <TabButton isActive={activeTab === 'contact'} onClick={() => setActiveTab('contact')} > Contact </TabButton> <hr /> <Activity mode={activeTab === 'home' ? 'visible' : 'hidden'}> <Home /> </Activity> <Activity mode={activeTab === 'contact' ? 'visible' : 'hidden'}> <Contact /> </Activity> </> ); }
Again, the Activity boundary let us preserve the Contact tab’s internal state without changing its implementation.
Pre-rendering content that’s likely to become visible
So far, we’ve seen how Activity can hide some content that the user has interacted with, without discarding that content’s ephemeral state.
But Activity boundaries can also be used to prepare content that the user has yet to see for the first time:
<Activity mode="hidden">
<SlowComponent />
</Activity>
When an Activity boundary is hidden during its initial render, its children won’t be visible on the page — but they will still be rendered, albeit at a lower priority than the visible content, and without mounting their Effects.
This pre-rendering allows the children to load any code or data they need ahead of time, so that later, when the Activity boundary becomes visible, the children can appear faster with reduced loading times.
Let’s look at an example.
In this demo, the Posts tab loads some data. If you press it, you’ll see a Suspense fallback displayed while the data is being fetched:
import { useState, Suspense } from 'react'; import TabButton from './TabButton.js'; import Home from './Home.js'; import Posts from './Posts.js'; export default function App() { const [activeTab, setActiveTab] = useState('home'); return ( <> <TabButton isActive={activeTab === 'home'} onClick={() => setActiveTab('home')} > Home </TabButton> <TabButton isActive={activeTab === 'posts'} onClick={() => setActiveTab('posts')} > Posts </TabButton> <hr /> <Suspense fallback={<h1>🌀 Loading...</h1>}> {activeTab === 'home' && <Home />} {activeTab === 'posts' && <Posts />} </Suspense> </> ); }
This is because App
doesn’t mount Posts
until its tab is active.
If we update App
to use an Activity boundary to show and hide the active tab, Posts
will be pre-rendered when the app first loads, allowing it to fetch its data before it becomes visible.
Try clicking the Posts tab now:
import { useState, Suspense, unstable_Activity as Activity } from 'react'; import TabButton from './TabButton.js'; import Home from './Home.js'; import Posts from './Posts.js'; export default function App() { const [activeTab, setActiveTab] = useState('home'); return ( <> <TabButton isActive={activeTab === 'home'} onClick={() => setActiveTab('home')} > Home </TabButton> <TabButton isActive={activeTab === 'posts'} onClick={() => setActiveTab('posts')} > Posts </TabButton> <hr /> <Suspense fallback={<h1>🌀 Loading...</h1>}> <Activity mode={activeTab === 'home' ? 'visible' : 'hidden'}> <Home /> </Activity> <Activity mode={activeTab === 'posts' ? 'visible' : 'hidden'}> <Posts /> </Activity> </Suspense> </> ); }
Posts
was able to prepare itself for a faster render, thanks to the hidden Activity boundary.
Pre-rendering components with hidden Activity boundaries is a powerful way to reduce loading times for parts of the UI that the user is likely to interact with next.
Deferring hydration of low-priority content
You can wrap part of your UI in a visible Activity boundary to defer mounting it on the initial render:
function Page() {
return (
<>
<Post />
<Activity mode="visible">
<Comments />
</Activity>
</>
)
}
During hydration, React will leave the visible Activity boundary unmounted while hydrating the rest of the page, improving the performance of higher-priority content. Once the high-priority content has fetched its code and data, and been rendered to the page, React will move on to mount any remaining visible Activity boundaries.
This feature is called Selective Hydration, and it’s an under-the-hood optimization of React that’s integrated with Suspense. You can read an architectural overview and watch a technical talk to learn more.
Troubleshooting
My hidden components have unwanted side effects
An Activity boundary hides its content by setting display: none
on its children and cleaning up any of their Effects. So, most well-behaved React components that properly clean up their side effects will already be robust to being hidden by Activity.
But there are some situations where a hidden component behaves differently than an unmounted one. Most notably, since a hidden component’s DOM is not destroyed, any side effects from that DOM will persist, even after the component is hidden.
As an example, consider a <video>
tag. Typically it doesn’t require any cleanup, because even if you’re playing a video, unmounting the tag stops the video and audio from playing in the browser. Try playing the video and then pressing Home in this demo:
import { useState } from 'react'; import TabButton from './TabButton.js'; import Home from './Home.js'; import Video from './Video.js'; export default function App() { const [activeTab, setActiveTab] = useState('video'); return ( <> <TabButton isActive={activeTab === 'home'} onClick={() => setActiveTab('home')} > Home </TabButton> <TabButton isActive={activeTab === 'video'} onClick={() => setActiveTab('video')} > Video </TabButton> <hr /> {activeTab === 'home' && <Home />} {activeTab === 'video' && <Video />} </> ); }
The video stops playing as expected.
Now, let’s say we wanted to preserve the timecode where the user last watched, so that when they tab back to the video, it doesn’t start over from the beginning again.
This is a great use case for Activity!
Let’s update App
to hide the inactive tab with a hidden Activity boundary instead of unmounting it, and see how the demo behaves this time:
import { useState, unstable_Activity as Activity } from 'react'; import TabButton from './TabButton.js'; import Home from './Home.js'; import Video from './Video.js'; export default function App() { const [activeTab, setActiveTab] = useState('video'); return ( <> <TabButton isActive={activeTab === 'home'} onClick={() => setActiveTab('home')} > Home </TabButton> <TabButton isActive={activeTab === 'video'} onClick={() => setActiveTab('video')} > Video </TabButton> <hr /> <Activity mode={activeTab === 'home' ? 'visible' : 'hidden'}> <Home /> </Activity> <Activity mode={activeTab === 'video' ? 'visible' : 'hidden'}> <Video /> </Activity> </> ); }
Whoops! The video and audio continue to play even after it’s been hidden, because the tab’s <video>
element is still in the DOM.
To fix this, we can add an Effect with a cleanup function that pauses the video:
export default function VideoTab() {
const ref = useRef();
useLayoutEffect(() => {
const videoRef = ref.current;
return () => {
videoRef.pause()
}
}, []);
return (
<video
ref={ref}
controls
playsInline
src="..."
/>
);
}
We call useLayoutEffect
instead of useEffect
because conceptually the clean-up code is tied to the component’s UI being visually hidden. If we used a regular effect, the code could be delayed by (say) a re-suspending Suspense boundary or a View Transition.
Let’s see the new behavior. Try playing the video, switching to the Home tab, then back to the Video tab:
import { useState, unstable_Activity as Activity } from 'react'; import TabButton from './TabButton.js'; import Home from './Home.js'; import Video from './Video.js'; export default function App() { const [activeTab, setActiveTab] = useState('video'); return ( <> <TabButton isActive={activeTab === 'home'} onClick={() => setActiveTab('home')} > Home </TabButton> <TabButton isActive={activeTab === 'video'} onClick={() => setActiveTab('video')} > Video </TabButton> <hr /> <Activity mode={activeTab === 'home' ? 'visible' : 'hidden'}> <Home /> </Activity> <Activity mode={activeTab === 'video' ? 'visible' : 'hidden'}> <Video /> </Activity> </> ); }
It works great! Our cleanup function ensures that the video stops playing if it’s ever hidden by an Activity boundary, and even better, because the <video>
tag is never destroyed, the timecode is preserved, and the video itself doesn’t need to be initialized or downloaded again when the user switches back to watch it.
This is a great example of using Activity to preserve ephemeral DOM state for parts of the UI that become hidden, but the user is likely to interact with again soon.
Our example illustrates that for certain tags like <video>
, unmounting and hiding have different behavior. If a component renders DOM that has a side effect, and you want to prevent that side effect when an Activity boundary hides it, add an Effect with a return function to clean it up.
The most common cases of this will be from the following tags:
<video>
<audio>
<iframe>
Typically, though, most of your React components should already be robust to being hidden by an Activity boundary. And conceptually, you should think of “hidden” Activities as being unmounted.
To eagerly discover other Effects that don’t have proper cleanup, which is important not only for Activity boundaries but for many other behaviors in React, we recommend using <StrictMode>
.
My hidden components have Effects that aren’t running
When an <Activity>
is “hidden”, all its children’s Effects are cleaned up. Conceptually, the children are unmounted, but React saves their state for later. This is a feature of Activity because it means subscriptions won’t be active for hidden parts of the UI, reducing the amount of work needed for hidden content.
If you’re relying on an Effect mounting to clean up a component’s side effects, refactor the Effect to do the work in the returned cleanup function instead.
To eagerly find problematic Effects, we recommend adding <StrictMode>
which will eagerly perform Activity unmounts and mounts to catch any unexpected side-effects.