<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 re-render in response to new props, 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.
Speeding up interactions during page load
React includes an under-the-hood performance optimization called Selective Hydration. It works by hydrating your app’s initial HTML in chunks, enabling some components to become interactive even if other components on the page haven’t loaded their code or data yet.
Suspense boundaries participate in Selective Hydration, because they naturally divide your component tree into units that are independent from one another:
function Page() {
return (
<>
<MessageComposer />
<Suspense fallback="Loading chats...">
<Chats />
</Suspense>
</>
)
}
Here, MessageComposer
can be fully hydrated during the initial render of the page, even before Chats
is mounted and starts to fetch its data.
So by breaking up your component tree into discrete units, Suspense allows React to hydrate your app’s server-rendered HTML in chunks, enabling parts of your app to become interactive as fast as possible.
But what about pages that don’t use Suspense?
Take this tabs example:
function Page() {
const [activeTab, setActiveTab] = useState('home');
return (
<>
<TabButton onClick={() => setActiveTab('home')}>
Home
</TabButton>
<TabButton onClick={() => setActiveTab('video')}>
Video
</TabButton>
{activeTab === 'home' && (
<Home />
)}
{activeTab === 'video' && (
<Video />
)}
</>
)
}
Here, React must hydrate the entire page all at once. If Home
or Video
are slower to render, they could make the tab buttons feel unresponsive during hydration.
Adding Suspense around the active tab would solve this:
function Page() {
const [activeTab, setActiveTab] = useState('home');
return (
<>
<TabButton onClick={() => setActiveTab('home')}>
Home
</TabButton>
<TabButton onClick={() => setActiveTab('video')}>
Video
</TabButton>
<Suspense fallback={<Placeholder />}>
{activeTab === 'home' && (
<Home />
)}
{activeTab === 'video' && (
<Video />
)}
</Suspense>
</>
)
}
…but it would also change the UI, since the Placeholder
fallback would be displayed on the initial render.
Instead, we can use Activity. Since Activity boundaries show and hide their children, they already naturally divide the component tree into independent units. And just like Suspense, this feature allows them to participate in Selective Hydration.
Let’s update our example to use Activity boundaries around the active tab:
function Page() {
const [activeTab, setActiveTab] = useState('home');
return (
<>
<TabButton onClick={() => setActiveTab('home')}>
Home
</TabButton>
<TabButton onClick={() => setActiveTab('video')}>
Video
</TabButton>
<Activity mode={activeTab === "home" ? "visible" : "hidden"}>
<Home />
</Activity>
<Activity mode={activeTab === "video" ? "visible" : "hidden"}>
<Video />
</Activity>
</>
)
}
Now our initial server-rendered HTML looks the same as it did in the original version, but thanks to Activity, React can hydrate the tab buttons first, before it even mounts Home
or Video
.
Thus, in addition to hiding and showing content, Activity boundaries help improve your app’s performance during hydration by letting React know which parts of your page can become interactive in isolation.
And even if your page doesn’t ever hide part of its content, you can still add always-visible Activity boundaries to improve hydration performance:
function Page() {
return (
<>
<Post />
<Activity>
<Comments />
</Activity>
</>
);
}
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.