<Activity>
Hides or shows part of the UI while keeping its state, so it can be restored or pre-rendered.
<Activity mode={visibility}>
{children}
</Activity>- Added in React 19.2.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$children | ReactNode | The UI you intend to show and hide. |
$modeoptional | 'visible' | 'hidden'= 'visible' | Whether the children are shown. 'hidden' hides them with display: none, destroys their Effects, and keeps their state and DOM; 'visible' reveals them with their previous state and re-creates their Effects. |
Return value
ReactNodeIts children — rendered normally when visible, or hidden with `display: none` while their state and DOM are kept alive.
Changelog
- PHP 19.2.0Introduced as a stable API for hiding and restoring parts of the UI without losing their state.
Conditionally rendering a subtree destroys it: unmounting throws away its state, its DOM, its scroll position and any work already done. Hiding it instead keeps all of that in memory while removing it from view — the children stay mounted, keep their state, and are visually hidden with display: none.
What changes when it is hidden
- Effects are destroyed, so subscriptions, timers and listeners are cleaned up exactly as if the tree unmounted.
- State and DOM are preserved, and re-created Effects run again on the way back to visible.
- Updates still flow in from new props, but at a lower priority than anything visible.
When to reach for it
Two shapes: keeping a screen the user navigated away from ready to return to with its inputs intact, and pre-rendering a screen they are likely to open next so its data, CSS and images are already warm.
What trips people up
Hidden children still occupy memory and still render on prop changes — hiding a hundred heavy screens is not free. And because the hiding mechanism is a CSS property, a hidden subtree whose output is nothing but text produces no DOM at all: there is no element on which to set display: none. Wrap text children in an element if they must survive being hidden.
Example
When to use it
- Keep a half-filled form mounted while the user checks another tab, so nothing is retyped.
- Pre-render the next step of a wizard so opening it is instant.
- Preserve list scroll position when navigating into a detail view and back.
- Warm the data and images of a likely next route without competing with visible rendering.
Frequently asked questions
How is this different from just rendering conditionally?
Conditional rendering unmounts the subtree and destroys its state and DOM, while hiding keeps both in memory and only removes the content from view. Coming back restores the previous state instead of starting from scratch.
Do Effects keep running while the children are hidden?
No — hiding destroys the children's Effects, so subscriptions, intervals and listeners are cleaned up exactly as they would be on unmount. They are created again when the subtree becomes visible.
Which React version do I need?
React 19.2 or newer. On earlier versions the import does not exist, and the closest equivalent is keeping the subtree mounted yourself behind a CSS class — which does not stop its Effects or deprioritise its updates.
More examples
Tabs that remember where you were
Typing notes, switching to Settings, ticking the checkbox and switching back leaves both the textarea contents and the checkbox state exactly as they were. Written with `tab === 'notes' ? <Notes /> : <Settings />` instead, each switch would unmount the other panel and reset it.
import { Activity, useState } from "react";
function Notes() {
const [value, setValue] = useState("");
return (
<textarea
rows={4}
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="unsaved notes"
/>
);
}
function Settings() {
const [alerts, setAlerts] = useState(false);
return (
<label>
<input
type="checkbox"
checked={alerts}
onChange={(e) => setAlerts(e.target.checked)}
/>{" "}
Email alerts {alerts ? "on" : "off"}
</label>
);
}
export default function Tabs() {
const [tab, setTab] = useState("notes");
return (
<div>
<button onClick={() => setTab("notes")}>Notes</button>
<button onClick={() => setTab("settings")}>Settings</button>
<Activity mode={tab === "notes" ? "visible" : "hidden"}>
<Notes />
</Activity>
<Activity mode={tab === "settings" ? "visible" : "hidden"}>
<Settings />
</Activity>
</div>
);
}Pre-rendering the next step
The summary step begins loading immediately, at lower priority than the visible cart. By the time Continue is pressed the promise has resolved, so the step appears without its loading fallback — the work happened while the user was reading the previous screen.
import { Activity, Suspense, use, useState } from "react";
const summaryPromise = new Promise((resolve) =>
setTimeout(() => resolve({ total: 42, currency: "EUR" }), 800)
);
function Summary() {
const data = use(summaryPromise);
return (
<p>
Total: {data.total} {data.currency}
</p>
);
}
export default function Checkout() {
const [step, setStep] = useState("cart");
return (
<div>
<h2>Checkout — {step}</h2>
<Activity mode={step === "cart" ? "visible" : "hidden"}>
<button onClick={() => setStep("summary")}>Continue</button>
</Activity>
{/* Rendered from the start, hidden until needed, so its data
is already resolved by the time the user gets here. */}
<Activity mode={step === "summary" ? "visible" : "hidden"}>
<Suspense fallback={<p>Loading summary…</p>}>
<Summary />
</Suspense>
</Activity>
</div>
);
}Hidden text needs an element
Both lines look the same when visible. While hidden, the wrapped version leaves a <span style="display: none"> in the DOM that can be inspected and revealed again, whereas the bare-text version leaves no node behind — which matters if you were relying on measuring or querying the hidden subtree.
import { Activity, useState } from "react";
// Returns bare text: while hidden there is no element to set
// display: none on, so nothing is rendered into the DOM at all.
function BareText() {
return "just a string";
}
// Wrapped in an element, so it can be hidden and restored.
function WrappedText() {
return <span>just a string</span>;
}
export default function TextVisibility() {
const [visible, setVisible] = useState(false);
return (
<div>
<button onClick={() => setVisible((v) => !v)}>
{visible ? "Hide" : "Show"}
</button>
<p>
Bare: <Activity mode={visible ? "visible" : "hidden"}><BareText /></Activity>
</p>
<p>
Wrapped: <Activity mode={visible ? "visible" : "hidden"}><WrappedText /></Activity>
</p>
</div>
);
}
Discussion