<Activity>
<Activity> lets you hide and reveal part of the UI while preserving its state.
<Activity mode={isVisible ? 'visible' : 'hidden'}>
<Sidebar />
</Activity>Reference
<Activity>
Wrap part of the component tree in <Activity> to control whether it is visible:
import { Activity } from 'react';
<Activity mode={isVisible ? 'visible' : 'hidden'}>
<Sidebar />
</Activity>Modes
An Activity boundary supports two modes:
- In
visiblemode, React renders the children, attaches their refs, and runs the setup functions for theiruseEffectanduseLayoutEffectcalls. - In
hiddenmode, React hides the children, detaches their refs, and runs the cleanup functions for theiruseEffectanduseLayoutEffectcalls. React preserves their state and renders updates at a lower priority than updates to visible content.
When a hidden Activity boundary becomes visible, React reveals its children with their previous state and runs their Effect setup functions again.
In React DOM, hiding an Activity boundary applies display: none to the nearest DOM elements inside the boundary. React preserves those elements while the boundary remains mounted.
Insertion Effects created with useInsertionEffect remain connected while an Activity boundary is hidden because styles may still be needed by the preserved DOM.
Props
children: The UI rendered by the Activity boundary.childrencan be any React node.- optional
mode: Either'visible'or'hidden'. Defaults to'visible'. See Modes for the behavior of each value. - optional
name: A string that identifies the Activity boundary in React Developer Tools.
Caveats
- Browser behavior associated with preserved DOM nodes can continue while the boundary is hidden. For example, audio and video can continue playing. Use an Effect cleanup function to stop this behavior. See an example below.
- React omits text-only output while an Activity boundary is hidden because a text node cannot receive
display: none. The text appears when the boundary becomes visible. - If an Activity boundary is inside
<ViewTransition>, changing it from hidden to visible as part of an update started withstartTransitionactivates theenteranimation. Changing it from visible to hidden as part of that update activates theexitanimation.
Usage
Activity is useful when part of the UI may become hidden and visible again. Unlike conditional rendering, hiding an Activity boundary preserves both React state and the DOM state of its children. Unlike hiding content only with CSS, Activity also cleans up the children’s Effects and deprioritizes their updates while they are hidden.
Use an Activity boundary when preserving that work is valuable—for example, for a tab the user is likely to revisit or a panel that can prepare data in the background. A hidden boundary retains its state and DOM nodes, so it continues using memory. If the content is unlikely to become visible again, conditionally rendering it may be preferable because unmounting allows React and the browser to release its resources.
Preserving state while content is hidden
When this condition becomes false, React removes <Sidebar> from the tree and discards its state:
{isShowingSidebar && <Sidebar />}Render the component inside an Activity boundary to preserve its state while it is hidden:
<Activity mode={isShowingSidebar ? 'visible' : 'hidden'}>
<Sidebar />
</Activity>In this example, expand the sidebar, hide it, and show it again. The expanded state is preserved.
import { Activity, useState } from 'react'; export default function App() { const [isShowingSidebar, setIsShowingSidebar] = useState(true); return ( <div className='layout'> <Activity mode={isShowingSidebar ? 'visible' : 'hidden'} > <Sidebar /> </Activity> <main> <button aria-controls='documentation-sidebar' aria-expanded={isShowingSidebar} onClick={() => setIsShowingSidebar(s => !s)} > {isShowingSidebar ? 'Hide' : 'Show'} sidebar </button> <h1>Main content</h1> </main> </div> ); } function Sidebar() { const [isExpanded, setIsExpanded] = useState(false); return ( <nav aria-label='Documentation' id='documentation-sidebar' > <button aria-controls='overview-sections' aria-expanded={isExpanded} onClick={() => setIsExpanded(e => !e)} > Overview <span aria-hidden='true' className='indicator'> {isExpanded ? '−' : '+'} </span> </button> {isExpanded && ( <ul id='overview-sections'> <li>Section 1</li> <li>Section 2</li> <li>Section 3</li> </ul> )} </nav> ); }
Changing mode preserves the state of the children. Removing the boundary or changing a child’s type, key, or position can reset its state.
Preserving DOM state while content is hidden
An Activity boundary also preserves state held by the browser in DOM nodes. This includes an uncontrolled input’s current value, scroll position, and media playback position.
In this example, enter a draft in the Contact section, switch sections, and then return to Contact. The <textarea> value remains because its DOM node was hidden rather than removed.
import { Activity, useState } from 'react'; export default function App() { const [activeSection, setActiveSection] = useState('contact'); return ( <> <div aria-label='Profile sections' className='section-buttons' role='group' > <SectionButton isActive={activeSection === 'home'} onClick={() => setActiveSection('home')} > Home </SectionButton> <SectionButton isActive={activeSection === 'contact'} onClick={() => setActiveSection('contact')} > Contact </SectionButton> </div> <Activity mode={activeSection === 'home' ? 'visible' : 'hidden'} > <Home /> </Activity> <Activity mode={activeSection === 'contact' ? 'visible' : 'hidden'} > <Contact /> </Activity> </> ); } function SectionButton({ isActive, onClick, children }) { return ( <button aria-pressed={isActive} onClick={onClick}> {children} </button> ); } function Home() { return <p>Welcome to my profile!</p>; } function Contact() { return ( <p> <label htmlFor='message'>Message</label> <textarea id='message' /> </p> ); }
Preserving DOM state also means that DOM behavior can continue while content is hidden. See Troubleshooting for DOM behavior that requires explicit cleanup.
Pre-rendering content that is likely to become visible
An Activity boundary can also prepare content before the user sees it. Content inside a hidden boundary renders at a lower priority without running Effects created with useEffect or useLayoutEffect. This lets the content load code and render-time data without delaying updates to visible content:
<Suspense fallback={<Loading />}>
<Activity mode={activeTab === 'posts' ? 'visible' : 'hidden'}>
<Posts />
</Activity>
</Suspense>If Posts suspends while reading code or data, React continues rendering the rest of the page. If that hidden work completes, switching to the Posts tab can reveal the content without waiting for the same work again.
The following example renders the Posts tab in a hidden Activity boundary when the page first loads. Wait briefly before selecting Posts. The list is available immediately because the hidden render started loading it in the background.
import { Activity, Suspense, use, useState } from 'react'; import { getPosts } from './data.js'; export default function App() { const [activeTab, setActiveTab] = useState('home'); return ( <> <div aria-label='Profile sections' role='group'> <button aria-pressed={activeTab === 'home'} onClick={() => setActiveTab('home')} > Home </button> <button aria-pressed={activeTab === 'posts'} onClick={() => setActiveTab('posts')} > Posts </button> </div> <Suspense fallback={<h1>Loading posts...</h1>}> <Activity mode={activeTab === 'home' ? 'visible' : 'hidden'} > <Home /> </Activity> <Activity mode={activeTab === 'posts' ? 'visible' : 'hidden'} > <Posts /> </Activity> </Suspense> </> ); } function Home() { return <p>Welcome to my profile!</p>; } function Posts() { const posts = use(getPosts()); return ( <ul> {posts.map(post => ( <li key={post.id}>{post.title}</li> ))} </ul> ); }
If the user selects Posts before the hidden render finishes, the nearest Suspense fallback appears until the data is ready. Activity improves the likely case where the background work finishes first; it does not guarantee that the content will always be ready.
Improving hydration performance
Activity boundaries also divide server-rendered pages into units that React can hydrate independently. This is related to the selective hydration behavior of <Suspense>, but it does not require displaying a fallback in the initial UI.
For example, without a boundary React hydrates this page as one unit:
function Page() {
return (
<>
<Post />
<Comments />
</>
);
}Wrapping Comments in an always-visible Activity boundary creates a separate hydration unit:
function Page() {
return (
<>
<Post />
<Activity>
<Comments />
</Activity>
</>
);
}The boundary is visible because the mode prop defaults to 'visible'. Its server-rendered HTML remains visible, but React can hydrate it independently from the surrounding page. If the user interacts with that content before React reaches it, React prioritizes hydrating the boundary.
You can also use visible and hidden Activity boundaries for tabbed content:
function Page() {
const [activeTab, setActiveTab] = useState('home');
return (
<>
<button onClick={() => setActiveTab('home')}>
Home
</button>
<button onClick={() => setActiveTab('video')}>
Video
</button>
<Activity mode={activeTab === 'home' ? 'visible' : 'hidden'}>
<Home />
</Activity>
<Activity mode={activeTab === 'video' ? 'visible' : 'hidden'}>
<Video />
</Activity>
</>
);
}React does not include initially hidden <Activity> content in server-rendered HTML. On the client, React hydrates the visible content first and renders the hidden content later at a lower priority. Initially visible boundaries are included in the server-rendered HTML and can be hydrated independently. This allows the visible tab and the controls around it to become interactive without waiting for React to render the initially hidden tab.
Troubleshooting
My hidden components have unwanted side effects
<Activity> hides DOM nodes without removing them. Browser-managed behavior from elements such as <video>, <audio>, and <iframe> can therefore continue while the boundary is hidden.
My hidden components have Effects that are not running
React runs cleanup functions for Effects created with useEffect and useLayoutEffect when an Activity boundary becomes hidden. It runs their setup functions again when the boundary becomes visible.
An Effect inside a hidden Activity boundary cannot remain active. Move ongoing work that must continue while the UI is hidden to a component outside the boundary, or keep the boundary visible.
If an Effect controls an external system, return a cleanup function so hiding the boundary disconnects from that system:
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => {
connection.disconnect();
};
}, []);Use <StrictMode> to find Effects that do not clean up correctly. Strict Mode performs an additional setup and cleanup cycle in development.