<Activity> - This feature is available in the latest Experimental version of React

Experimental Feature

This API is experimental and is not available in a stable version of React yet.

You can try it by upgrading React packages to the most recent experimental version:

  • react@experimental
  • react-dom@experimental
  • eslint-plugin-react-hooks@experimental

Experimental versions of React may contain bugs. Don’t use them in production.

<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 then 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.

See more examples below.

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 ContactTab() {
  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 ContactTab 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 HomeTab from './HomeTab.js';
import ContactTab from './ContactTab.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'}>
        <HomeTab />
      </Activity>
      <Activity mode={activeTab === 'contact' ? 'visible' : 'hidden'}>
        <ContactTab />
      </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.

Note

Only Suspense-enabled data sources will be fetched during pre-rendering. They include:

  • Data fetching with Suspense-enabled frameworks like Relay and Next.js
  • Lazy-loading component code with lazy
  • Reading the value of a cached Promise with use

Activity does not detect data that is fetched inside an Effect.

The exact way you would load data in the Posts component above depends on your framework. If you use a Suspense-enabled framework, you’ll find the details in its data fetching documentation.

Suspense-enabled data fetching without the use of an opinionated framework is not yet supported. The requirements for implementing a Suspense-enabled data source are unstable and undocumented. An official API for integrating data sources with Suspense will be released in a future version of React.


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 rest of the page has fetched its code and data and been rendered, 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

Preventing hidden content from having 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 HomeTab from './HomeTab.js';
import VideoTab from './VideoTab.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' && <HomeTab />}
      {activeTab === 'video' && <VideoTab />}
    </>
  );
}

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 HomeTab from './HomeTab.js';
import VideoTab from './VideoTab.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'}>
        <HomeTab />
      </Activity>
      <Activity mode={activeTab === 'video' ? 'visible' : 'hidden'}>
        <VideoTab />
      </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();

useEffect(() => {
const videoRef = ref.current;

return () => {
videoRef.pause()
}
}, []);

return (
<video
ref={ref}
controls
playsInline
src="..."
/>

);
}

Let’s see the new behavior:

import { useState, unstable_Activity as Activity } from 'react';
import TabButton from './TabButton.js';
import HomeTab from './HomeTab.js';
import VideoTab from './VideoTab.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'}>
        <HomeTab />
      </Activity>
      <Activity mode={activeTab === 'video' ? 'visible' : 'hidden'}>
        <VideoTab />
      </Activity>
    </>
  );
}

It’s working 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>.


Effects don’t mount when an Activity is hidden

When an <Activity> is “hidden”, all 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.