React's Activity Component, Six Months In
↓ Jump to section
React 19.2 shipped the Activity component on 2025-10-01 as a stable, non-experimental API. Its job is narrow: keep a subtree mounted while it isn't on screen, so component state and DOM survive a visibility toggle. Narrow APIs are the ones that end up everywhere, and six months in, that's what's happened.
The public surface hasn't changed since the 19.2 release. Two props, mode: 'visible' | 'hidden' and children. What has changed is that the community has now put enough miles on it to know where it helps, where it doesn't, and which of its caveats actually bite in production.
What it replaced
Before Activity, preserving a component's state across visibility toggles meant one of three hacks.
You could mount the component inside a CSS display: none container, but that costs a render on every parent update regardless of whether anyone sees it. You could lift every piece of state up into a parent that survives the unmount, but that turns local state into structural state, which accumulates into the "context soup" problem most apps outgrow. Or you could reach for react-keep-alive or a homegrown equivalent, all of which worked around React rather than with it.
Activity is the inside-React solution. The component stays committed, but React unmounts its Effects and deprioritises its work:
import { Activity } from 'react';
<Activity mode={isShowingSidebar ? 'visible' : 'hidden'}>
<Sidebar />
</Activity>
Switching from hidden to visible doesn't remount; scroll position, form input, and local state all survive. The hidden subtree still receives re-renders, but at low priority. It won't block input handling or a higher-priority update elsewhere in the tree.
The patterns that stuck
Four use cases emerged as load-bearing in production.
Tabs, drawers, and multi-step wizards. The canonical case. Users expect that switching back to the tab they were on finds their input where they left it. Pre-Activity, "keep the tab state on the parent" was the polite answer; in practice it meant lifting seven pieces of local state up one level for each new tab. Wrapping each panel in Activity with the right mode erases the problem.
Pre-rendering a likely-next route. Render the target route as hidden while the user is on the current page. React warms up its component tree, Suspense boundaries start fetching data, and CSS/images preload, all at low priority. When the user navigates, the switch to visible is instant because the tree is already there. This is the pattern the React team themselves demoed at 19.2 launch and it's become standard inside opinionated routers.
Back-navigation state retention. Paired with your router, Activity means the view you came from doesn't lose state when you return. A product list you filtered and scrolled halfway down is still filtered and scrolled when you hit Back. This used to be a router-level feature or a state-management hack; now it's three lines.
Selective hydration for below-the-fold content. Activity boundaries hydrate independently, like Suspense boundaries do. Wrapping "below the fold" content in hidden Activity lets the above-the-fold content hydrate first and the rest hydrate when (or if) it comes into view.
The footguns people hit
The API is quiet but the semantics have sharp edges.
Hidden does not mean inert. This is the biggest one. Activity only unmounts React Effects. If your subtree has a <video>, a <audio>, an <iframe>, a WebSocket, a setInterval, or any imperative subscription, that resource keeps running after you flip to hidden. A video will keep streaming; an iframe will keep rendering; a polling interval will keep polling. The fix is to add your own cleanup that responds to visibility, not just to unmount:
useEffect(() => {
const video = videoRef.current;
return () => video?.pause();
}, []);
Most of the production bugs I've seen on Activity are this bug: someone wrapped a media player or an iframe in Activity, assumed "hidden means paused", and shipped an invisible video silently chewing bandwidth.
Text-only children produce no DOM. If your hidden component returns a bare string, there's nothing for React to preserve. Wrap text in an element if you care about its position in the tree when it comes back.
Over-application is a real cost. Activity keeps trees mounted. Mounted trees still receive props, still re-render on context changes, and still hold memory. Wrapping every conditional in Activity because it's available costs you memory and render cycles that you didn't need to spend. Reach for it when state preservation or pre-render latency actually matters; use a ternary otherwise.
Deferred props can pile up silently. Hidden subtrees render at low priority, but they still render. If the parent passes cheap-looking props that trigger expensive child re-renders, those renders accumulate in the background. Instrument with the React DevTools profiler before assuming "hidden = free".
What Activity doesn't do
It doesn't handle data staleness. If your hidden tab contained a list of notifications from 20 minutes ago, Activity will happily show you those 20-minute-old notifications when you come back; it preserved them. Pair with TanStack Query's placeholderData (or your data layer's equivalent), which keeps the last-known data visible while a fresh fetch runs in the background. The two are complementary: TanStack Query handles your data, Activity handles your DOM.
It doesn't replace your router's caching. Router-level prefetch and stale-route caching solve the "data is ready when you arrive" problem; Activity solves the "DOM and local state are ready when you arrive" problem. Most apps want both.
It doesn't trigger View Transitions on its own, but it does compose with them. Toggling a mode change inside startTransition lets the accompanying <ViewTransition> run its enter/exit animation on the swap. The early framing that Activity "doesn't integrate with View Transitions" has not aged well; the React Labs post that shipped alongside the stable API demoes the composition directly.
Where it goes next
The React team has flagged additional modes as future work. A "visible but inactive" mode (for a modal's underlying content) has been mentioned in release notes but hasn't landed in 19.2.x. The stable API has been deliberately small, which is part of why the patterns around it have converged so fast: there's not much API surface to disagree about, and the constraints push you toward the right shape.
Six months in, Activity is the component I reach for whenever I would have reached for a CSS trick, a state-preservation library, or a lift-state-up refactor. It's not a silver bullet (the media-and-subscription caveat alone rules out casual use) but when you need it, it replaces three hundred lines of workaround with two props and a wrapper.
Official reference: react.dev/reference/react/Activity. The React 19.2 release post has the full picture alongside Partial Pre-rendering and SSR batching, both of which pair with Activity in interesting ways.