React 19.2’s <Activity> is a good fit for switching among a small number of agent-session panes when users expect drafts, scroll position, and expanded tool details to survive. In hidden mode React preserves component state and DOM, cleans up Effects, and processes hidden updates at lower priority. It does not keep an Effect-based stream alive, free hidden DOM memory, or create a security boundary.
Know exactly what hidden means
React 19.2 supports visible and hidden Activity modes. The official React 19.2 announcement states that hidden children are hidden, their Effects are unmounted, and their updates are deferred until React has no higher-priority work. When visible again, React restores state and mounts Effects.
The detailed <Activity> reference adds an important implementation fact: React applies display: none to retained child DOM. This means:
- hook state survives;
- input and scroll-related DOM state can survive;
- effect subscriptions and timers are cleaned up;
- the hidden DOM still consumes memory;
- DOM-native side effects can continue unless cleanup stops them.
Think “logically unmounted effects with retained UI state,” not “paused process.”
Keep the agent workflow outside the pane
A production agent run should live on the server or in a durable client-side store, not inside the pane’s useEffect. Hiding an Activity cleans up Effects, so a WebSocket or fetch owned solely by that pane will disconnect. That is correct React behavior but often the wrong workflow architecture.
Separate three lifetimes:
| Lifetime | Owner | Hidden behavior |
|---|---|---|
| Agent workflow | server state machine | continues or cancels by explicit policy |
| Run-event connection | session-level transport manager | may remain connected or reconnect |
| Pane UI | Activity subtree | preserves local presentation state |
Our durable-agent guide covers the server-side state machine. The pane should subscribe to a run store and render it; it should not be the source of truth for whether tools execute.
Build a bounded session workspace
Use one Activity per retained session and a stable key:
"use client";
import { Activity, useState } from "react";
interface SessionSummary {
readonly id: string;
readonly title: string;
}
export function AgentWorkspace({
sessions,
}: {
sessions: readonly SessionSummary[];
}) {
const [activeId, setActiveId] = useState(sessions[0]?.id ?? null);
return (
<div className="workspace">
<SessionTabs
sessions={sessions}
activeId={activeId}
onSelect={setActiveId}
/>
{sessions.map((session) => (
<Activity
key={session.id}
mode={session.id === activeId ? "visible" : "hidden"}
>
<AgentSessionPane sessionId={session.id} />
</Activity>
))}
</div>
);
}
The key ties state to the session identity. Reusing a position with the wrong key can show a previous draft under a different session. When a session closes, remove its Activity from the array so React truly unmounts it and releases the retained subtree.
Cap the number of retained sessions. A hidden transcript with thousands of nodes, code editors, charts, and iframes still occupies browser memory. Use windowed transcripts and an explicit least-recently-used rule: keep perhaps the product-approved working set, persist drafts, and unmount older panes. The actual cap must come from measurement on target devices, not an arbitrary tutorial number.
Make every Effect safe to mount again
Effects inside an Activity will clean up on hide and run again on reveal. A connection effect must unsubscribe completely:
import { useEffect } from "react";
function AgentSessionPane({ sessionId }: { sessionId: string }) {
const snapshot = useAgentRunSnapshot(sessionId);
useEffect(() => {
const subscription = runEvents.subscribe(sessionId);
return () => subscription.unsubscribe();
}, [sessionId]);
return <SessionView snapshot={snapshot} />;
}
The shared runEvents manager owns durable event state and deduplicates reconnection by sequence. The Effect owns only this pane’s subscription. If the subscription itself must stay active while every pane is hidden, place the owner above the Activity boundaries.
Use <StrictMode> in development. The Activity documentation recommends it for finding effects that do not tolerate cleanup and remount. Test timers, observers, event listeners, sockets, audio, and third-party widgets.
Clean up DOM-native side effects
Because DOM nodes remain, hiding is not equivalent to removing a <video>, <audio>, or <iframe>. React’s Activity reference demonstrates that media may keep playing after the pane is hidden and recommends cleanup, using a layout effect when the action must align with the visual hide.
Agent interfaces can have similar problems:
- a sandboxed preview iframe continues computing;
- speech synthesis or audio playback continues;
- an embedded terminal retains a worker;
- a canvas animation keeps scheduling frames;
- browser focus remains inside a subtree that is becoming hidden.
Stop these resources in Effect cleanup and restore only what is safe. Move focus to the selected tab or visible pane before hiding the old one. Confirm hidden content is absent from keyboard navigation and assistive-technology reading order in supported browsers.
Use Activity for UI state, not confidential retention
Hidden DOM can still contain rendered prompt text and tool output in process memory. Activity is not an access-control mechanism. On logout, tenant change, permission revocation, or session deletion, unmount the subtree and clear relevant client stores and caches.
Do not pre-render a confidential session merely because the user might click it. Authorize data at its source, return minimal fields, and avoid embedding secrets in props. A CSS-hidden pane does not protect data from browser extensions, compromised scripts, or someone with developer tools.
For shared devices, decide whether unsent draft preservation is appropriate at all. Sensitive workflows may intentionally reset state when users leave.
Understand pre-rendering limitations
Activity can pre-render hidden content at low priority, but the React reference states that only data reads which activate Suspense—such as a Promise read with use—participate. Data fetched later inside an Effect is not pre-rendered by Activity.
Do not add an Effect fetch merely to warm the next session. Use the framework’s server data and Suspense architecture, or a reviewed client data layer. In Next.js 16, Cache Components can use Activity internally for navigation state, but application-level session retention still needs explicit memory and privacy policy; see Cache Components for AI apps.
Decide when to unmount instead
Unmount rather than hide when:
- the state must reset for correctness or security;
- the subtree is large and unlikely to return soon;
- a third-party component cannot clean up reliably;
- there may be hundreds of sessions;
- the workflow should terminate when the UI leaves;
- preserving a stale form would mislead the user.
Ordinary conditional rendering remains simpler. Activity earns its cost only when state preservation and likely return materially improve the experience.
Test behavior, not just rendering
Create an integration test that opens two sessions, types an unsent draft, expands tool details, switches tabs, receives background events, and switches back. Assert that:
- the draft and expansion state are restored;
- run events are neither lost nor duplicated;
- hidden pane effects unsubscribe;
- the durable server run follows explicit cancellation policy;
- audio, iframes, workers, timers, and focus are cleaned up;
- closing a session removes its subtree;
- logout removes all confidential UI state;
- memory remains bounded through a long switching session.
Profile with React’s Performance Tracks, introduced in 19.2, and browser heap snapshots. Measure switching responsiveness and retained DOM rather than assuming hidden work is free.