September 30, 2025

Activity, the new React component

parHugo
Hugo

4 minutes reading

Activity, the new React component
🇫🇷 This post is also available in french

Freshly announced on the React canary channel, the new Activity component aims to make conditional rendering of components much easier, while allowing their state to be preserved even while visually hidden. Let’s explore how it works.

Installation

Let’s start by creating a simple React project using Vite:

bun create vite@latest

We’ll need to update the package.json to use the canary version of React and React DOM.

Use case

Let's implement a frequent use case: managing a multi-step form.

Basic usage

Up to now, the typical way to handle this would look like:

function App() {
  const [step, setStep] = useState(1);

return (

<div
  style={{
    margin: '1rem',
    display: 'flex',
    alignItems: 'center',
    flex: 1,
    flexDirection: 'column',
    gap: '1rem',
  }}
>
  <div style={{ display: 'flex', gap: '1rem' }}>
    <button type="button" onClick={() => setStep(1)}>
      Step 1
    </button>
    <button type="button" onClick={() => setStep(2)}>
      Step 2
    </button>
  </div>
  {step === 1 && <Step1 />}
  {step === 2 && <Step2 />}
</div>
); }

const Step1 = () => {
  const [name, setName] = useState("");

  return (
    <input
      value={name}
      onChange={(e) => setName(e.target.value)}
      name="name"
      placeholder="Name"
    />
  );
};
const Step2 = () => {
  const [address, setAddress] = useState("");

return (

<input
  value={address}
  onChange={(e) => setAddress(e.target.value)}
  name="address"
  placeholder="Address"
/>
); };

While this code works, it has an important UX drawback: if you fill in a field and then navigate to the next step, the input’s state is lost. This happens because the component is unmounted. To avoid losing state, we could instead hide the component rather than unmounting it, ensuring it always remains in the React tree.

App.tsx
function App() {
  const [step, setStep] = useState(1);

  return (
    <div
      style={{
        margin: "1rem",
        display: "flex",
        alignItems: "center",
        flex: 1,
        flexDirection: "column",
        gap: "1rem",
      }}
    >
      <div style={{ display: "flex", gap: "1rem" }}>
        <button type="button" onClick={() => setStep(1)}>
          Step 1
        </button>
        <button type="button" onClick={() => setStep(2)}>
          Step 2
        </button>
      </div>
      <div style={step === 2 ? { display: "none" } : {}}>
        <Step1 />
      </div>
      <div style={step === 1 ? { display: "none" } : {}}>
        <Step2 />
      </div>
    </div>
  );
}

This approach works, but it’s not ideal—especially if your components contain side-effects, event listeners, or interactive content such as video. You’d need to manage activation/deactivation yourself using props, including controlling your effect hooks (like useEffect or useLayoutEffect) based on those props. Although that’s feasible, it quickly adds complexity.

Enter the Activity component

This is precisely where the Activity component comes in. Its job is simple: it hides a DOM node with display: none, keeps the React node's state intact, and only executes effects when the component is visible. In effect, it allows your component to be “partially” mounted and unmounted.

App.tsx
function App() {
  const [step, setStep] = useState(1);

  return (
    <div
      style={{
        margin: "1rem",
        display: "flex",
        alignItems: "center",
        flex: 1,
        flexDirection: "column",
        gap: "1rem",
      }}
    >
      <div style={{ display: "flex", gap: "1rem" }}>
        <button type="button" onClick={() => setStep(1)}>
          Step 1
        </button>
        <button type="button" onClick={() => setStep(2)}>
          Step 2
        </button>
      </div>
      <Activity mode={step === 1 ? "visible" : "hidden"}>
        <Step1 />
      </Activity>
      <Activity mode={step === 2 ? "visible" : "hidden"}>
        <Step2 />
      </Activity>
    </div>
  );
}

Now, text input is preserved when switching between steps. If you inspect the DOM, you’ll see that a hidden input is simply set to display: none.

To dig deeper, let’s look at how effects behave with some simple console.logs.

step2.tsx
const Step2 = () => {
  const [address, setAddress] = useState("");

  useEffect(() => {
    console.log("Step2 mounted");

    return () => {
      console.log("Step2 unmounted");
    };
  }, []);

  useLayoutEffect(() => {
    console.log("Step2 layout mounted");

    return () => {
      console.log("Step2 layout unmounted");
    };
  }, []);

  return (
    <input
      value={address}
      onChange={(e) => setAddress(e.target.value)}
      name="address"
      placeholder="Address"
    />
  );
};

If you reload the page, you’ll notice none of the logs are printed, even though the input exists in the DOM. When you navigate to step 2, the effects run, and if you go back to step 1, their cleanup functions are called.

Pre-rendering and remote data

To fetch remote data, a common approach is to use a useEffect. However, as we've seen, effects do not run for hidden components wrapped with Activity. To handle this, we can use the use hook.

step2.tsx
const fetchAddress = new Promise<string>((resolve) => {
  resolve("Some street name");
});

const Step2 = () => {
  const addressData = use(fetchAddress);
  const [address, setAddress] = useState(addressData);

  return (
    <input
      value={address}
      onChange={(e) => setAddress(e.target.value)}
      name="address"
      placeholder="Address"
    />
  );
};

With this setup, the fetchAddress promise is fired as soon as Step2 gets instantiated, and the resolved value appears in your input—right at page load.

Conclusion

This brand new Activity component will make certain complex use cases much more straightforward to handle in your apps. It's extremely intuitive to use, though you may need to refactor parts of existing projects—especially where effect behavior changes.

Have an idea for a React project or need some support? Get in touch with us!

Resources:

You may also like

Premier Octet vous accompagne dans le développement de vos projets avec react