Command Palette

Search for a command to run...

Hectal
PHASE 16Advanced React 19.2+ ~20 min· topic 10 of 10

Topic 16.10

Hiding Without Losing State: Activity

In one line

<Activity mode="visible" | "hidden"> hides part of the UI without unmounting it: hidden content keeps its state and DOM (hidden with display: none), its effects are cleaned up while hidden and re-created when shown, and React can pre-render hidden content in the background at low priority.

Think of it like this

Switching between apps on your phone. When you leave a chat app to check the weather, the chat isn't closed: your half-typed message is still there when you come back. But while it's in the background, the phone pauses it so it doesn't use the battery. <Activity> does the same for parts of your page: hidden content is kept with its state, but paused.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

<Activity>
A React 19.2 component that can hide its children without removing them, keeping their state.
mode
Activity's prop: "visible" shows the children, "hidden" hides and pauses them.
Unmount
React removing a component completely: its state and DOM are destroyed and effects cleaned up.
Pre-rendering
Rendering content in the background before it's shown, so it appears instantly later.
Effect cleanup
The function an effect returns, which stops what the effect started (timers, subscriptions, connections).
Low priority
Work React does only after everything visible is done, so it never slows down the screen.

Step by step

01Conditional rendering loses state

With {tab === "chat" && <Chat />}, switching to Home unmounts Chat. The draft message is gone when you return.

02Wrap it in Activity instead

Always render Chat, and let Activity decide whether it's visible.

App.jsxwhole filejsx
import { Activity } from "react";

<Activity mode={tab === "chat" ? "visible" : "hidden"}>
  <Chat />
</Activity>

03What happens when it hides

React sets display: none on Chat's DOM, runs its effect cleanups (the connection closes), and keeps its state. If its props change while hidden, it re-renders at low priority.

What happens when it hidesdiagram
Rendering diagram…

04What happens when it shows again

React removes the display: none, and re-runs the effects (the connection opens again). The draft is still in the input.

05Pre-render what comes next

Render the next tab hidden from the start. Its components render and create DOM in the background; the first time the user opens it, it's ready.

Tabs.jsxwhole filejsx
<Activity mode={tab === "reports" ? "visible" : "hidden"}>
  <Reports />  {/* rendered in the background before the first click */}
</Activity>

Try it yourself

  1. 1

    Compare with conditional rendering

    In the first example, replace the Activity with {tab === "chat" && <Chat />}. Predict the last log line (Draft value:). It's empty, because Chat was unmounted. Note also that the display: line now throws, because the input no longer exists: wrap it in if (el).

  2. 2

    Pause a timer while hidden

    Add a useEffect to Chat that logs tick every 200 ms with setInterval and clears it in the cleanup. Raise wait to 1500 and switch to Home. Predict: ticks stop while hidden and restart when you return.

  3. 3

    Memoise the hidden tree

    In the pre-render example, wrap Reports in memo. Predict the log after the click: the second Reports rendered line disappears, because its props didn't change.

Code & diagrams

State kept, effects paused New tab

Going Home disconnects the chat (the effect cleanup ran) but keeps the input in the DOM with `display: none`. Chat also renders once while hidden, at low priority, after the visible update is committed. Coming back reconnects, and the draft "Hi" is still there. With `{tab === "chat" && <Chat />}` the draft would be gone.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

  Chat rendered, draft = ""
  Chat effect: connect
  Chat rendered, draft = "Hi"
--- go Home
  Chat effect: disconnect
  Chat rendered, draft = "Hi"
Input still in the DOM: true | display: none
--- back to Chat
  Chat rendered, draft = "Hi"
  Chat effect: connect
Draft value: Hi
Pre-rendering a tab before it's opened New tab

Reports rendered and created its DOM while hidden, but its effect didn't run. Clicking only reveals it (and re-renders it, because its parent rendered): the effect runs now, when it's visible.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

App rendered
Reports rendered
Reports list already in the DOM: true
--- click Show reports
App rendered
Reports rendered
Reports effect ran: now visible
Three ways to hide, compareddiagram
Rendering diagram…

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

An effect with no cleanup

In Chat, write useEffect(() => { document.title = "Chat open"; }, []) with no cleanup, then switch to Home.

terminal
$ Preview
── what you'll see ──
The tab title still says "Chat open" while the Home page is showing.

Myth vs fact

Myth

Activity hidden is the same as display: none.

Fact

It uses display: none for the DOM, but it also cleans up effects and lowers the priority of hidden re-renders, so hidden content doesn't keep working in the background.

Myth

Hidden content doesn't render at all.

Fact

It renders, at the lowest priority, when its props change or when pre-rendering. Only its effects are paused.

Myth

Activity replaces conditional rendering everywhere.

Fact

Every hidden tree keeps its state and DOM in memory. Use it where keeping state matters; use conditional rendering when it doesn't.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Activity is built on the Offscreen fiber type React has used internally for years (for Suspense). Hidden subtrees are rendered on the OffscreenLane, the lowest priority lane, and their passive and layout effects are disconnected while hidden.

  • ▸

    Before 19.2, this API was experimental under the name unstable_Activity (and earlier Offscreen). The stable name is Activity with mode set to "visible" or "hidden".

  • ▸

    During server rendering, hidden Activity content isn't included in the initial HTML; it renders on the client later. Visible Activity boundaries also act as selective hydration boundaries, so React can hydrate parts of the page independently.

  • ▸

    Hidden DOM still exists: document.querySelector finds it, and its <video> and <audio> elements keep playing unless your effect cleanup pauses them.

Remember this

  1. 1

    The problem it solves. The usual way to hide something is conditional rendering: {tab === "chat" && <Chat />}. When the condition becomes false, React unmounts Chat: its state (the draft message, scroll position, form fields) is destroyed, and showing it again starts from scratch, re-running its setup and data loading. Hiding with CSS (style={{ display: "none" }}) keeps the state, but the hidden component keeps running: effects stay subscribed, timers tick, and every update re-renders it at full priority.

  2. 2

    **<Activity>** (imported from react, React 19.2) is a boundary with a mode prop: "visible" (the default) or "hidden". <Activity mode={tab === "chat" ? "visible" : "hidden"}><Chat /></Activity>. When hidden: React keeps the component's state and its DOM, and hides the DOM with display: none (by setting the style on the top DOM nodes inside the boundary). When visible again, everything is exactly as it was.

  3. 3

    Effects in hidden trees. When content becomes hidden, React runs the cleanup of every effect inside it (useEffect and useLayoutEffect), as if it had unmounted, but keeps the state. When it becomes visible, React re-runs the effects, as if mounting again. So subscriptions, timers and connections stop while hidden. This is the same mount → cleanup → mount pattern StrictMode tests in development (Topic 7.4), so components with correct cleanups already work. Things that aren't effects don't stop on their own: a playing <video> keeps playing when hidden with display: none, so pause it in an effect cleanup.

  4. 4

    Hidden content still renders, at low priority. If a hidden tree receives new props, React re-renders it in the background, at the lowest priority, after all visible work is done, so it never slows down what the user sees. Its effects don't run until it becomes visible.

  5. 5

    Pre-rendering. You can render content that hasn't been shown yet with mode="hidden". React renders it in the background and creates its DOM, so showing it later is instant. If the hidden content suspends (lazy code or data with Suspense-enabled sources, Topic 14.3), React loads it in the background too, without showing a fallback. Effects don't run during pre-rendering, so data fetched in useEffect won't start until it's visible: pre-rendering helps with code and Suspense-enabled data, not effect-based fetching.

  6. 6

    When to use it. Tabs, sidebars and multi-step forms where users switch back and forth and expect their place to be kept; pre-rendering the likely next screen; keeping a back-navigated page alive. Each hidden tree costs memory (its state and DOM stay), so don't keep dozens of heavy screens hidden. For a list of many items where state can be recreated, conditional rendering is still simpler.

Explain it without notes

01

What problem does <Activity> solve that conditional rendering and CSS hiding don't?

02

What happens to state, DOM and effects when an Activity becomes hidden, and when it becomes visible again?

03

What is pre-rendering with Activity, and what are its limits?

04

When should you not use Activity?

Practice

01

Build tabs "Profile" and "Settings" where each tab keeps its form input when you switch away and back.

02

A hidden VideoPlayer keeps playing sound after its tab is hidden with Activity. Fix it.

Trade-offs

  • ↔

    Activity keeps users' place and makes switching instant, at the cost of memory for every hidden tree and some low-priority background rendering.

  • ↔

    Pre-rendering makes the next screen instant but spends CPU and bandwidth on screens the user may never open.

Done when you can

  • I can hide content with Activity and keep its state.

  • I can explain what happens to effects when content is hidden and shown.

  • I can pre-render a likely next screen.

  • I can compare Activity with conditional rendering and CSS hiding.

  • I know to write effect cleanups so hidden content truly pauses.