The React Activity component hides part of the tree without unmounting it. Wrap a subtree in <Activity mode="hidden"> and React sets display: none on it, tears down its Effects and keeps both its React state and its DOM state, so a half-typed textarea is still there when the subtree comes back. It shipped in React 19.2 in October 2025, and 19.3.0, the npm latest as of October 2026, fixes several of its early bugs.
That makes it the replacement for isOpen && <Panel /> wherever losing state on the way out is a bug: tab panels, master-detail views, wizard steps, anything a user backs into. It also gives you a supported way to render the next screen before the user asks for it.
What does Activity do that conditional rendering does not?
Conditional rendering destroys the subtree. React discards the component state, the browser discards the DOM, and when the condition flips back everything mounts from scratch. For a settings panel that is fine. For a Contact tab with a half-written message it is a lost message.
Activity takes one prop, mode, which is either 'visible' or 'hidden' and defaults to 'visible'. When the boundary is hidden, the Activity reference describes four things happening at once. React hides the children with display: "none". It runs the cleanup of every Effect inside, which closes subscriptions and timers. It keeps the state, both the useState values and the DOM, so typed input and other DOM state survive. And it keeps re-rendering the hidden children when their props change, but at a lower priority than anything on screen. When the boundary becomes visible again, React shows the existing DOM and recreates the Effects.
The reference’s framing is that a hidden Activity is “conceptually unmounted”. React holds the state as a saved snapshot, and the Effects go because an unmounted component should not be holding a subscription or a timer open.
import { Activity, useState } from 'react';
import { Home } from './Home';
import { Contact } from './Contact';
type Tab = 'home' | 'contact';
export function Tabs() {
const [active, setActive] = useState<Tab>('home');
return (
<>
<nav>
<button onClick={() => setActive('home')}>Home</button>
<button onClick={() => setActive('contact')}>Contact</button>
</nav>
<Activity mode={active === 'home' ? 'visible' : 'hidden'}>
<Home />
</Activity>
<Activity mode={active === 'contact' ? 'visible' : 'hidden'}>
<Contact />
</Activity>
</>
);
}
Contact can be an ordinary component with a plain <textarea> and no state management at all. Switch to Home and back, and the draft is still in the box. Nothing in Contact had to change. The parent that owns the layout decides what is hidden; the child never finds out.
How do you pre-render the next screen with it?
If an Activity boundary is hidden on its first render, React still renders the children. It does so at a lower priority than the visible content and without mounting their Effects. The React 19.2 release notes put it as: hidden mode “defers all updates until React has nothing left to work on”. So the hidden tab’s code is evaluated, its images and stylesheets can start loading, and if it suspends on data that data is requested too.
One limit applies to that last clause. Only data read through something that activates a Suspense boundary, such as a Promise read with use, is fetched during pre-rendering. The reference is explicit that Activity does not detect data fetched inside an Effect. A useEffect(() => { fetch(...) }) in the hidden tab does nothing until the tab is shown, because the Effect never ran.
import { Activity, Suspense, use } from 'react';
const postsPromise = fetch('/api/posts').then((r) => r.json());
function Posts() {
const posts = use(postsPromise) as Array<{ id: string; title: string }>;
return (
<ul>
{posts.map((p) => <li key={p.id}>{p.title}</li>)}
</ul>
);
}
export function Dashboard({ tab }: { tab: 'home' | 'posts' }) {
return (
<Suspense fallback={<p>Loading</p>}>
<Activity mode={tab === 'home' ? 'visible' : 'hidden'}>
<Home />
</Activity>
<Activity mode={tab === 'posts' ? 'visible' : 'hidden'}>
<Posts />
</Activity>
</Suspense>
);
}
With tab === 'home', the /api/posts request goes out during the initial render. By the time the user clicks Posts the data is usually in hand and the tab appears without a fallback. If your data layer fetches in Effects, as many query libraries do by default, you get the state preservation benefit but not this one.
What keeps running while an Activity is hidden?
Anything the DOM does on its own. React removes the Effects but leaves the elements in place, so a <video> or <audio> element keeps playing with no picture, and an embedded <iframe> carries on with whatever it was loading. Refs stay attached to the hidden elements.
The fix is an Effect whose cleanup stops the behaviour, because cleanup is what runs on hide. The reference recommends useLayoutEffect for this rather than useEffect. A regular Effect’s cleanup can be delayed by a Suspense boundary re-suspending or by a View Transition, so the video would carry on for a moment after its picture had gone.
import { useLayoutEffect, useRef } from 'react';
export function Player({ src }: { src: string }) {
const ref = useRef<HTMLVideoElement>(null);
useLayoutEffect(() => {
const video = ref.current;
return () => {
video?.pause();
};
}, []);
return <video ref={ref} src={src} controls playsInline />;
}
To find components that misbehave, the reference points at <StrictMode>, which in development performs Activity unmounts and mounts to surface side effects that leak. A component that survives StrictMode’s double Effect cycle is usually already fine inside an Activity.
Two smaller behaviours follow from the display: none mechanism. A hidden Activity whose child renders only text produces nothing in the DOM at all, because there is no element to put display: none on. And display: none releases the layout space; if you need a placeholder to keep the page from reflowing, that is a wrapper element and a style of your own, not something Activity does for you.
What changed in React 19.3?
The React 19.3 release on 9 September 2026 is mostly about <ViewTransition> and Fragment refs going stable, but the changelog carries a run of Activity fixes that matter if you adopted it on 19.2.
useSyncExternalStore no longer misses store mutations that happened while an Activity tree was hidden, which matters for any store library built on that hook when its subscriber sits inside a hidden tab. A <title> inside an Activity is no longer left hoisted into <head> after the boundary goes hidden. Errors thrown inside a hidden Activity no longer escape into the visible UI. Portal contents rendered inside a hidden Activity are now hidden too, so a modal owned by a hidden tab stops leaking onto the screen. And React Server Components gained support for <Activity> in the Flight protocol.
The ViewTransition pairing is also now stable. Per the ViewTransition reference, when a <ViewTransition> inside an Activity becomes visible as the result of an update inside startTransition, its enter animation runs, and when it becomes hidden its exit animation runs, with the child’s state kept throughout. If you are animating whole-page navigations in a multi-page site instead, our write-up of cross-document view transitions without a router covers the browser-level version.
When should you still use conditional rendering?
Keep cond && <X /> for subtrees the user will not come back to. Keep it for anything large enough that holding it in memory is a cost, since a hidden Activity still owns its DOM and still re-renders on prop changes, just later. And keep it for long lists, where wrapping each item would multiply that cost. Activity suits a handful of screens the user moves between.
On React 19.2 or later, make Activity the default for tab panels, split views and multi-step forms where the user can go back, and wrap the hidden steps in a Suspense boundary with use based data reads so they pre-render. Pair it with useActionState for the form submission itself; our post on useActionState forms that keep their input covers that half. Add a useLayoutEffect cleanup to any component that owns media or an iframe. If you depend on useSyncExternalStore inside hidden content, go straight to 19.3.0 rather than stopping at 19.2. And if your data fetching lives in Effects, you are buying state preservation only; the faster navigations arrive when those reads move to use.
Whoooop builds and upgrades React applications for UK clients, including moving long-lived codebases onto React 19 and the newer rendering APIs. If a tab-heavy or form-heavy interface is losing state or feeling slow between screens, our React development work covers the audit and the migration.