<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 and runs the setup functions for theiruseEffectanduseLayoutEffectcalls. - In
hiddenmode, React hides the children 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.
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
- Hiding an Activity boundary retains its state and DOM nodes, so React does not reclaim all memory associated with the hidden subtree.
- 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 runs cleanup functions for Effects created with
useEffectanduseLayoutEffectwhen an Activity boundary becomes hidden. Insertion Effects created withuseInsertionEffectremain connected because styles may still be needed by the preserved DOM. - React detaches refs in a hidden Activity boundary and reattaches them when the boundary becomes visible.
- Content initially rendered inside
<Activity mode="hidden">is not included in server-rendered HTML. React renders it on the client at a lower priority after hydrating visible content. - 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
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
Content inside a hidden Activity boundary renders at a lower priority without
running Effects created with useEffect or useLayoutEffect. This allows the
content to begin loading code and render-time data before it becomes visible:
<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.
Deep Dive
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.
For initially visible <Activity> boundaries, React includes the content in the
server-rendered HTML but can hydrate the boundary independently from surrounding
content. If the user interacts with the boundary before hydration reaches it,
React prioritizes hydrating that boundary.
An always-visible <Activity> boundary lets React hydrate that subtree
independently:
function Page() {
return (
<>
<Post />
<Activity>
<Comments />
</Activity>
</>
);
}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.