cf.completefrontend
Open lab My progress
THE REACT FIELD GUIDE

Hiding and pre-rendering with Activity

Hide part of a React page with Activity and keep its state and DOM, stop its effects while hidden, pre-render a tab, and compare it with conditional rendering.

By the end, you can
  • 01
    Hide a tab without losing itWrap a tab in <Activity mode> so a draft, an open section, and the rest of its state and DOM survive while it's hidden.
  • 02
    Predict what runs while hiddenExplain why a hidden tab's effects are cleaned up and run again when it's shown, and why it still re-renders, at a lower priority.
  • 03
    Choose how to hidePre-render a tab the reader will open next, and pick between Activity, conditional rendering, and CSS display: none.

Hide part of the page without losing it

Apps hide things all the time: the tab you aren't on, a closed sidebar, the page behind a dialog. When the reader comes back, they expect it as they left it. <Activity> is a component that comes with React for this. You wrap part of the page in it and give it a mode: "visible" shows its children, "hidden" hides them but keeps them. Here a support app has two tabs. The result is live: write something in the chat box, open Profile, then come back to Chat.

Two tabs, each in an Activity boundaryPop out in the code editor (opens in a new tab)JSX
import { Activity, useState } from "react";import Chat from "./Chat.jsx";import Profile from "./Profile.jsx"; export default function SupportApp() {  const [tab, setTab] = useState("chat");  return (    <>      <nav>        <button onClick={() => setTab("chat")}>Chat</button>        <button onClick={() => setTab("profile")}>Profile</button>      </nav>      <Activity mode={tab === "chat" ? "visible" : "hidden"}>        <Chat />      </Activity>      <Activity mode={tab === "profile" ? "visible" : "hidden"}>        <Profile />      </Activity>    </>  );}
ResultLoading…

Your message is still there. Line 1 imports Activity from React. Line 6 keeps which tab is open. Lines 13 to 15 wrap Chat in an Activity boundary whose mode is "visible" while the chat tab is open and "hidden" otherwise, and lines 16 to 18 do the same for Profile. The draft lives in Chat's own state, on line 4 of the Chat.jsx tab.

Both tabs are in the page the whole time. The hidden one is still mounted, still has its state, and still has its DOM elements; React only hides those elements. Activity became a stable part of React in React 19.2, with these two modes.

The tree with the Profile tab open. SupportApp has two Activity boundaries. The left one is hidden and holds Chat, which still has its draft "Hi Asha". The right one is visible and holds Profile, on screen.SupportAppActivity "hidden"Activity "visible"Chat, draft "Hi Asha"Profilekept, with display: noneon screen
With Profile open, the Chat boundary is hidden. Chat isn't removed: it keeps its draft and its DOM, and React hides that DOM with display: none.
Real-life analogyA laptop's sleep

Closing a laptop's lid doesn't shut it down. The screen goes dark, the network drops, and programs pause, but your open windows stay in memory. Open the lid and you're back in the sentence you were writing, and the laptop reconnects by itself.

In real life: You close the lid, and the screen goes dark
In React: mode="hidden": React hides the tab's elements
In real life: Your open documents and half-written email stay in memory
In React: The tab keeps its state and its DOM
In real life: While it sleeps, the laptop drops its Wi-Fi and stops its programs
In React: The tab's effects are cleaned up: connections close, timers stop
In real life: You open the lid, and everything is where you left it, back online
In React: mode="visible": the tab shows again, and its effects run again
The main idea

<Activity mode="hidden"> hides its children without unmounting them. Their state and DOM stay, React hides the DOM with display: none, and their effects are cleaned up until mode is "visible" again. A tab that starts hidden is rendered in the background, ready for when it's opened.

Conditional rendering throws the tab away

The usual way to show one tab is conditional rendering with &&. Here is the same app written that way. Write a message, open Profile, and come back:

The same tabs with &&Pop out in the code editor (opens in a new tab)JSX
import { useState } from "react";import Chat from "./Chat.jsx";import Profile from "./Profile.jsx"; export default function SupportApp() {  const [tab, setTab] = useState("chat");  return (    <>      <nav>        <button onClick={() => setTab("chat")}>Chat</button>        <button onClick={() => setTab("profile")}>Profile</button>      </nav>      {tab === "chat" && <Chat />}      {tab === "profile" && <Profile />}    </>  );}
ResultLoading…

The draft is gone, and the counter under it says 0 characters. When you open Profile, line 13 renders false instead of <Chat />, so React unmounts Chat: it removes its elements and throws its state away. Coming back renders a brand-new Chat, whose state starts at useState("") again. As Preserving and resetting state showed, React keeps state only while a component stays at its place in the tree.

Sometimes that's what you want, like a form that should start empty each time it opens. When it isn't, people reach for workarounds: lift the draft up to the parent, or save it somewhere. Activity keeps it where it is, without changing Chat at all.

A hidden tab keeps its state and its DOM

Some of what a reader leaves behind isn't in React state at all. It lives in the DOM elements themselves: the text in a <textarea> without a value prop, whether a <details> section is open, how far a list is scrolled. Because Activity keeps the elements, it keeps those too:

Notes that live only in the DOMPop out in the code editor (opens in a new tab)JSX
import { Activity, useState } from "react"; function Notes() {  return (    <section id="notes">      <details>        <summary>Shipping help</summary>        <p>Orders ship in 2 days.</p>      </details>      <textarea placeholder="Your notes" />    </section>  );} export default function SupportApp() {  const [showNotes, setShowNotes] = useState(true);  function logStyle() {    console.log(document.getElementById("notes").getAttribute("style"));  }  return (    <>      <button onClick={() => setShowNotes(!showNotes)}>{showNotes ? "Hide" : "Show"} notes</button>      <button onClick={logStyle}>Log the style</button>      <Activity mode={showNotes ? "visible" : "hidden"}>        <Notes />      </Activity>    </>  );}
ResultLoading…

Lines 6 to 9 are a <details> section, and line 10 a <textarea> with no value and no state: React never knows what you type in it. Open Shipping help, type a note, then press Hide notes and Show notes. The section is still open and your note is still there, because the elements never left the page.

Press Hide notes again, then Log the style. Line 18 logs the style attribute of the section, and the console says display: none !important;. Show the notes and log it again: it's now empty. That one style is all React does to the DOM to hide it. Here is the code that does it in React DOM, from ReactFiberConfigDOM.js:

hideInstance and hideTextInstance, from ReactFiberConfigDOM.js in React 19.3.1JavaScript
export function hideInstance(instance: Instance): void {
  // TODO: Does this work for all element types? What about MathML? Should we
  // pass host context to this method?
  instance = instance as any as HTMLElement;
  const style = instance.style;
  // $FlowFixMe[method-unbinding]
  if (typeof style.setProperty === 'function') {
    style.setProperty('display', 'none', 'important');
  } else {
    style.display = 'none';
  }
}

export function hideTextInstance(textInstance: TextInstance): void {
  textInstance.nodeValue = '';
}

In plain words:

  1. Line 1: hideInstance gets one DOM element, the top element of something to hide. React calls it for each top element inside the hidden boundary, not for every element.
  2. Lines 2 and 3 are a note from React's team, and line 6 a note for their type checker.
  3. Lines 7 and 8 set display: none with important, so the page's own CSS can't show it again.
  4. Lines 9 and 10 are a fallback for elements whose style has no setProperty.
  5. Lines 14 to 16: plain text has no style, so hideTextInstance empties the text instead. A hidden component that returns only text shows nothing.

When the boundary is visible again, React's unhideInstance, a few lines further down, puts back the element's own display from its style prop, or clears it when the prop doesn't set one.

Effects stop while a tab is hidden

A hidden tab keeps its state, but nothing in it should keep running: no open chat connection, no timer, no subscription. So Activity cleans up a hidden tab's effects. Here the chat tab connects to a room in an effect, in the Chat.jsx tab, and counts the seconds it has been online. Open the console under the result first:

A chat tab that connects in an effectPop out in the code editor (opens in a new tab)JSX
import { Activity, useState } from "react";import Chat from "./Chat.jsx";import Profile from "./Profile.jsx"; export default function SupportApp() {  const [tab, setTab] = useState("chat");  const [room, setRoom] = useState("#general");  return (    <>      <nav>        <button onClick={() => setTab("chat")}>Chat</button>        <button onClick={() => setTab("profile")}>Profile</button>        <button onClick={() => setRoom("#orders")}>Move to #orders</button>      </nav>      <Activity mode={tab === "chat" ? "visible" : "hidden"}>        <Chat room={room} />      </Activity>      <Activity mode={tab === "profile" ? "visible" : "hidden"}>        <Profile />      </Activity>    </>  );}
ResultLoading…

In the Chat.jsx tab, the effect on lines 8 to 15 logs Connect to #general, starts a timer that adds one to seconds every second, and returns a cleanup that logs Disconnect from #general and stops the timer. Line 6 logs every render, so the console line Chat renders for #general counts up with every tick. Try this:

  1. Wait for online for 3 s, and type a draft.
  2. Open Profile. The console prints Disconnect from #general: hiding ran the cleanup. The render count stops growing, because the timer stopped.
  3. Wait a few seconds and open Chat. The console prints Connect to #general again, and the heading shows the same number of seconds as when you left, then counts on. Your draft is there too. The state was kept; only the effect stopped and started again.

You'll also see a Disconnect and a second Connect right when the chat appears, at the start and each time you come back. That's StrictMode in development: it runs a new effect, its cleanup, and the effect again, to check that your cleanup undoes the effect. It does the same when Activity shows a tab again. In production, each line appears once.

Three moments of the chat tab. Visible: the effect connects, and the counter reaches 3 seconds. Hidden: the cleanup disconnects, and the counter stays at 3. Visible again: the effect connects again, and the counter goes on from 3 to 4.visibleConnectonline for 3 sthe effect runshiddenDisconnectstays at 3 sits cleanup runsvisibleConnect3 s, then 4 sthe effect runs again
Hiding runs the effect's cleanup, so the chat disconnects and its timer stops. Showing runs the effect again. The state, the draft and the seconds, stays the whole time.

Hidden content still renders when its props change. Open Profile, then press Move to #orders (line 13 changes the room prop). The console prints Chat renders for #orders while the chat is hidden, but no Connect: React renders the hidden tab so it's up to date, at a lower priority than anything on screen, and runs its effects only when you open it. Open Chat, and it connects to #orders.

One case needs your help. Some elements do things on their own, outside React: a <video> or <audio> keeps playing, and an <iframe> keeps running its page. Unmounting removed them, which stopped them. Activity keeps them, so a hidden video would keep playing. Give it an effect whose cleanup pauses it:

Pause a video when its tab is hidden (a fragment)JSX
function VideoTab() {
  const videoRef = useRef(null);

  useLayoutEffect(() => {
    const video = videoRef.current;
    return () => video.pause();
  }, []);

  return <video ref={videoRef} src="/tea-ceremony.mp4" controls />;
}

This fragment needs a video file, so it doesn't run here. Lines 4 to 7 are a layout effect whose cleanup, on line 6, pauses the video. Hiding runs that cleanup at once, and the video keeps its place, because the element stays. react.dev suggests a layout effect here because the pause belongs with the moment the tab disappears. Exercise 5 has a runnable case of the same problem.

Render a tab before anyone opens it

A boundary can be hidden from the very first render. Then React still renders its children, at a lower priority than the visible page, and without running their effects. That's pre-rendering: anything the tab needs while rendering, like data read with use or a component's code, starts loading before the reader asks. The orders below come from a pretend server in the data.js tab, which answers after one second. Press Run again, wait a moment, then open Orders:

The orders tab renders hidden, before the clickPop out in the code editor (opens in a new tab)JSX
import { Activity, Suspense, useState } from "react";import Orders from "./Orders.jsx";import Profile from "./Profile.jsx"; export default function SupportApp() {  const [tab, setTab] = useState("profile");  return (    <>      <nav>        <button onClick={() => setTab("profile")}>Profile</button>        <button onClick={() => setTab("orders")}>Orders</button>      </nav>      <Activity mode={tab === "profile" ? "visible" : "hidden"}>        <Profile />      </Activity>      <Activity mode={tab === "orders" ? "visible" : "hidden"}>        <Suspense fallback={<p>Loading orders…</p>}>          <Orders />        </Suspense>      </Activity>    </>  );}
ResultLoading…

The orders appear at once. Line 6 opens on Profile, so the boundary on lines 16 to 20 starts hidden. React renders Orders anyway, which calls use(fetchData("/orders")) on line 5 of its tab, so the console prints Load /orders as the page opens. The Suspense boundary on lines 17 to 19 waits for the data out of sight. By the time you click, the orders are ready. Click within the first second and you'll see Loading orders… for what's left of it.

Compare it with the same tabs and conditional rendering:

The orders tab loads only when it's openedPop out in the code editor (opens in a new tab)JSX
import { Suspense, useState } from "react";import Orders from "./Orders.jsx";import Profile from "./Profile.jsx"; export default function SupportApp() {  const [tab, setTab] = useState("profile");  return (    <>      <nav>        <button onClick={() => setTab("profile")}>Profile</button>        <button onClick={() => setTab("orders")}>Orders</button>      </nav>      {tab === "profile" && <Profile />}      {tab === "orders" && (        <Suspense fallback={<p>Loading orders…</p>}>          <Orders />        </Suspense>      )}    </>  );}
ResultLoading…

Nothing loads until you click. Then line 14 mounts the boundary, Orders asks for the data, and you wait a full second behind Loading orders….

Two rows. With Activity: the page opens and the hidden Orders tab loads its data; one second later the orders are ready, still hidden; the click shows them at once. With conditional rendering: nothing loads when the page opens; the click starts the load; the orders show one second after the click.Activity, hidden at firstLoad /orders1 s: ready, hiddenclick: at onceConditional renderingnothing loadsclick: Load /orders1 s later: shown
A hidden Activity renders its tab in the background on the first render, so the orders load before anyone asks, and the click shows them at once. With conditional rendering, loading only starts on the click.

Pre-rendering only loads what's read while rendering. A request in an effect, like the ones in Fetching data, waits until the tab is shown, because hidden content runs no effects. That's one more reason the Suspense way of reading data, through a cache, works well with Activity.

Activity, conditional rendering, or display: none

You could also hide a tab with CSS, by giving its wrapper display: none, the way the Styles and classes lesson does in plain JavaScript. That keeps the component mounted too, so how is it different? Here are three stopwatches, one for each way. Let them run for a few seconds, then hide all three, wait, and show them:

One stopwatch, hidden three waysPop out in the code editor (opens in a new tab)JSX
import { Activity, useEffect, useState } from "react"; function Stopwatch({ name }) {  const [seconds, setSeconds] = useState(0);  useEffect(() => {    const id = setInterval(() => setSeconds((s) => s + 1), 1000);    return () => clearInterval(id);  }, []);  return <p>{name}: {seconds} s</p>;} export default function ThreeWays() {  const [show, setShow] = useState(true);  return (    <>      <button onClick={() => setShow(!show)}>{show ? "Hide" : "Show"} all three</button>      {show && <Stopwatch name="Conditional" />}      <div style={{ display: show ? "block" : "none" }}>        <Stopwatch name="CSS display: none" />      </div>      <Activity mode={show ? "visible" : "hidden"}>        <Stopwatch name="Activity" />      </Activity>    </>  );}
ResultLoading…

Lines 3 to 10 are the stopwatch: an effect that adds one second every second, with a cleanup that stops it. After you show them again:

  • Conditional (line 17) starts from 0. It was unmounted, and its state with it.
  • CSS display: none (lines 18 to 20) counted the whole time. It was only out of sight; its effect never stopped. Every timer, connection, and subscription in a tab hidden this way keeps running, and every update renders at full speed.
  • Activity (lines 21 to 23) shows the number it had when you hid it, then goes on. Its state stayed; its effect stopped and started again.
A grid of three ways to hide a component. Conditional rendering: the state is thrown away, the DOM is removed, the effects are cleaned up. CSS display: none: the state and the DOM are kept, and the effects keep running. Activity: the state and the DOM are kept, and the effects are cleaned up.stateDOMeffectsConditionalthrown awayremovedcleaned updisplay: nonekeptkeptkeep runningActivitykeptkeptcleaned up
Only Activity keeps the state and the DOM while it stops the effects. Conditional rendering throws everything away; CSS hides the elements but leaves every effect running.

In the playground, the chat from the effects section, without its render log, is hidden each of the three ways. Choose a way, wait a few seconds, type a draft, then hide the chat, wait, and show it. Watch the counter, the draft, and the console.

Playground: three ways to hide the chat
The chat, hidden with ActivityPop out in the code editor (opens in a new tab)JSX
import { Activity, useState } from "react";import Chat from "./Chat.jsx"; export default function SupportApp() {  const [visible, setVisible] = useState(true);  return (    <>      <button onClick={() => setVisible(!visible)}>{visible ? "Hide" : "Show"} the chat</button>      <Activity mode={visible ? "visible" : "hidden"}>        <Chat room="#general" />      </Activity>    </>  );}
ResultLoading…
Try it yourself
Activity: lines 9 to 11 hide the chat

Wait for a few seconds online and type a draft, then hide the chat, wait, and show it. The console prints Disconnect on hide and Connect on show, with StrictMode's extra pair. The counter shows the seconds it had when you hid it, then counts on, and the draft is still there.

Each choice is the same app and the same Chat.jsx, the effects example's chat without its render log, so the console shows only effects and cleanups. The result runs React 19.3.1 in StrictMode, like every example in this lesson, so the chat's first Connect comes with an extra Disconnect and Connect. Choosing a way starts the app over.

How to read the table: each row is one question, answered for each way of hiding.

Three ways to hide a tab
QuestionConditional renderingCSS display: noneActivity
Is the state kept while hidden?No. The component unmounts, and its state is thrown away.Yes. The component stays mounted.Yes. React keeps it until the tab comes back.
Is the DOM kept?No. React removes the elements.Yes. Your style hides the wrapper.Yes. React sets display: none !important on each top element.
Do effects run while hidden?No. Their cleanups ran when it unmounted.Yes. Timers and connections keep going.No. Cleanups run on hide, and effects run again on show.
Does it render while hidden?No. It doesn't exist.Yes, as fast as visible content.Yes, when its props change, at a lower priority than visible content.
Can it render before it's first shown?No. It renders when it's shown.Yes, effects and all, at full priority.Yes. Hidden on the first render, it pre-renders at low priority, with no effects.

Hidden content isn't free: it keeps its memory, and it re-renders when its props change. Keep hidden what the reader is likely to come back to, or open next, and unmount the rest.

A support app with tabs that remember

Here is everything together. Each tab is hidden in the way that suits it, and the chat and orders tabs are the same files as above:

A support app with three tabsPop out in the code editor (opens in a new tab)JSX
import { Activity, Suspense, useState } from "react";import Chat from "./Chat.jsx";import Orders from "./Orders.jsx";import Profile from "./Profile.jsx"; const tabs = [  { id: "chat", label: "Chat" },  { id: "orders", label: "Orders" },  { id: "profile", label: "Profile" },]; export default function SupportApp() {  const [tab, setTab] = useState("chat");  return (    <>      <nav>        {tabs.map(({ id, label }) => (          <button key={id} onClick={() => setTab(id)} aria-pressed={tab === id}>            {label}          </button>        ))}      </nav>      <Activity mode={tab === "chat" ? "visible" : "hidden"}>        <Chat room="#general" />      </Activity>      <Activity mode={tab === "orders" ? "visible" : "hidden"}>        <Suspense fallback={<p>Loading orders…</p>}>          <Orders />        </Suspense>      </Activity>      {tab === "profile" && <Profile />}    </>  );}
ResultLoading…

Lines 6 to 10 list the tabs, and lines 17 to 21 make a button for each, with aria-pressed telling screen readers which one is open. The chat on lines 23 to 25 keeps its draft and disconnects while hidden. The orders on lines 26 to 30 start hidden, so they pre-render and load as the app opens. Profile, on line 31, has nothing to keep and nothing to load, so plain conditional rendering is enough.

Write a draft, open Orders (already loaded), then Profile, then Chat: the draft is there, and the console shows Disconnect from #general when you left the chat and Connect to #general when you came back, with StrictMode's extra pair in development.

Which way should it hide?
  • A checkout form that must start empty every time it opens
  • Fifty closed chat windows the reader won't open again
  • A small menu with nothing worth keeping, opened and closed by a button
  • A chat tab with a half-written message the reader will come back to
  • The tab the reader will likely open next, which reads its data with use
  • A video tab that should keep its place when the reader comes back
  • A decorative picture that hides on narrow screens with a media query
  • Buttons hidden when the page prints, with @media print
Try it yourself
0 of 8 correct

Sort each situation: conditional rendering, Activity, or CSS display: none.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "Hidden means unmounted." No. A hidden Activity keeps its components mounted, with their state and DOM. Only their effects are cleaned up.
  • "Activity is the same as display: none." No. CSS only hides the elements, and every effect keeps running. Activity also stops the effects and lowers the priority of hidden renders.
  • "A hidden tab doesn't render at all." No. It renders when its props change, and on its first render if it starts hidden, at a lower priority than what's on screen.
  • "Pre-rendering loads data from effects too." No. Hidden content runs no effects, so only data read while rendering, like use with a cached promise, starts loading early.
  • "Activity stops everything the tab does." No. It stops effects. A timer you start in a click handler, or a <video> that's playing, keeps going unless an effect's cleanup stops it.
  • "Keeping every tab hidden is always better." No. Hidden content takes memory and still re-renders. Unmount what the reader won't come back to, and anything that should start fresh.
  • "The extra Disconnect and Connect mean my code is wrong." No. StrictMode adds them in development to check your cleanup. In production, each runs once.

Practice exercises

The exercises use the same support app and the same pretend server. Change the code with Edit & run, and press Run again to start over.

Exercise 1 · Warm-upPredict the draft after a tab switch

These tabs use &&. You type Hi in the chat box, open Profile, and come back to Chat. What is in the draft box? Answer first, then press Edit & run and try it.

import { useState } from "react";
import Chat from "./Chat.jsx";
import Profile from "./Profile.jsx";

export default function SupportApp() {
  const [tab, setTab] = useState("chat");
  return (
    <>
      <nav>
        <button onClick={() => setTab("chat")}>Chat</button>
        <button onClick={() => setTab("profile")}>Profile</button>
      </nav>
      {tab === "chat" && <Chat />}
      {tab === "profile" && <Profile />}
    </>
  );
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upKeep the draft

    Change these tabs so the draft survives a trip to Profile, without changing Chat. Which mode does the tab that isn't selected get?

    import { useState } from "react";import Chat from "./Chat.jsx";import Profile from "./Profile.jsx"; export default function SupportApp() {  const [tab, setTab] = useState("chat");  return (    <>      <nav>        <button onClick={() => setTab("chat")}>Chat</button>        <button onClick={() => setTab("profile")}>Profile</button>      </nav>      {tab === "chat" && <Chat />}      {tab === "profile" && <Profile />}    </>  );}
    ResultLoading…

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticePredict the effect log

      The chat is open and connected to #general. You click Profile. Which line about the connection does the console print?

      import { Activity, useState } from "react";import Chat from "./Chat.jsx";import Profile from "./Profile.jsx"; export default function SupportApp() {  const [tab, setTab] = useState("chat");  const [room, setRoom] = useState("#general");  return (    <>      <nav>        <button onClick={() => setTab("chat")}>Chat</button>        <button onClick={() => setTab("profile")}>Profile</button>        <button onClick={() => setRoom("#orders")}>Move to #orders</button>      </nav>      <Activity mode={tab === "chat" ? "visible" : "hidden"}>        <Chat room={room} />      </Activity>      <Activity mode={tab === "profile" ? "visible" : "hidden"}>        <Profile />      </Activity>    </>  );}
      ResultLoading…

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeLoad the orders before the click

        Make the orders start loading as the app opens, so the tab is ready when the reader opens it. After your change, you click Orders 2 seconds after the page opens. How long does Loading orders… show?

        import { Suspense, useState } from "react";import Orders from "./Orders.jsx";import Profile from "./Profile.jsx"; export default function SupportApp() {  const [tab, setTab] = useState("profile");  return (    <>      <nav>        <button onClick={() => setTab("profile")}>Profile</button>        <button onClick={() => setTab("orders")}>Orders</button>      </nav>      {tab === "profile" && <Profile />}      {tab === "orders" && (        <Suspense fallback={<p>Loading orders…</p>}>          <Orders />        </Suspense>      )}    </>  );}
        ResultLoading…

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeStop the radio when its tab is hidden

          The radio keeps playing after you leave its tab: the console prints Playing the radio… every second. Fix the Radio.jsx tab so hiding the tab stops it, and showing it plays on. After your fix, how many times does the console print Playing the radio… while the tab is hidden?

          import { Activity, useState } from "react";import Radio from "./Radio.jsx";import Profile from "./Profile.jsx"; export default function SupportApp() {  const [tab, setTab] = useState("radio");  return (    <>      <nav>        <button onClick={() => setTab("radio")}>Radio</button>        <button onClick={() => setTab("profile")}>Profile</button>      </nav>      <Activity mode={tab === "radio" ? "visible" : "hidden"}>        <Radio />      </Activity>      <Activity mode={tab === "profile" ? "visible" : "hidden"}>        <Profile />      </Activity>    </>  );}
          ResultLoading…

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeStart the checkout form empty

            The checkout form should be empty every time it opens, but it remembers the last address. Fix Cart. Which way of hiding gives an empty form each time it opens?

            import { Activity, useState } from "react";import CheckoutForm from "./CheckoutForm.jsx"; export default function Cart() {  const [open, setOpen] = useState(false);  return (    <>      <button onClick={() => setOpen(true)}>Check out</button>      <Activity mode={open ? "visible" : "hidden"}>        <CheckoutForm onClose={() => setOpen(false)} />      </Activity>    </>  );}
            ResultLoading…

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask what the hidden part keeps (state, DOM) and what stops (effects).

              The Activity quiz · 8 questionsScore: first tries count
              1. Question 1 of 8A tab is inside <Activity mode="hidden">. What happens to its DOM elements?

                Choose an answer to see the explanation.

              2. Question 2 of 8A component in a tab connects to a chat room in an effect. The tab becomes hidden, then visible. What does the effect do?

                Choose an answer to see the explanation.

              3. Question 3 of 8All three stopwatches tick for 3 seconds. You hide them for 2 seconds, then show them. What do they show right after?

                Read the code, then predictJSX
                {show && <Stopwatch name="Conditional" />}
                <div style={{ display: show ? "block" : "none" }}>
                  <Stopwatch name="CSS display: none" />
                </div>
                <Activity mode={show ? "visible" : "hidden"}>
                  <Stopwatch name="Activity" />
                </Activity>

                Choose an answer to see the explanation.

              4. Question 4 of 8A tab starts inside <Activity mode="hidden"> and reads its orders with use(fetchData("/orders")). When does the request start?

                Choose an answer to see the explanation.

              5. Question 5 of 8The hidden tab loads its orders in a useEffect instead. When does that request start?

                Choose an answer to see the explanation.

              6. Question 6 of 8A hidden tab's props change, like the chat room it shows. What does React do?

                Choose an answer to see the explanation.

              7. Question 7 of 8What does this render?

                Read the code, then predictJSX
                function Greeting() {
                  return "Hello, Ravi";
                }
                
                <Activity mode="hidden">
                  <Greeting />
                </Activity>

                Choose an answer to see the explanation.

              8. Question 8 of 8Which React release made <Activity> a stable API?

                Choose an answer to see the explanation.

              Key takeaways

              • <Activity mode="visible"> shows its children; mode="hidden" hides them without unmounting. It's stable since React 19.2.
              • Hidden children keep their state and their DOM, so drafts, open sections, and scroll positions survive. React hides the DOM with display: none !important.
              • Hiding cleans up their effects; showing runs them again. Things started outside effects need a cleanup of their own.
              • Hidden children still re-render when their props change, at a lower priority than what's on screen.
              • A boundary that starts hidden pre-renders its children, so data read with use starts loading before the click.
              • Use conditional rendering for what should start fresh, CSS for pure styling, and Activity for what the reader will come back to.

              Remember the one-liner.
              Hidden keeps the state and the DOM, stops the effects, and renders quietly in the background.

              Coming next: View transitions. It animates between screens and states, with React's ViewTransition and the browser's View Transition API.

              CompleteFrontend Clear concepts. Working examples.