AccueilClients

Applications et sites

  • Application métiersIntranet, back-office...
  • Applications mobilesAndroid & iOS
  • Sites InternetSites marketings et vitrines
  • Expertises techniques

  • React
  • Expo / React Native
  • Next.js
  • Node.js
  • Directus
  • TypeScript
  • Open SourceBlogContactEstimer

    30 septembre 2025

    Activity, the new React component

    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>
    ); }
    
    

    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:

    À découvrir également

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

    Discuter de votre projet react
    18 avenue Parmentier
    75011 Paris
    +33 1 43 57 39 11hello@premieroctet.com
    ContactMentions légales
    Suivez nos aventures

    GitHub

    X

    Flux RSS

    Bluesky

    Navigation
    Nos expertises métiers

    Applications et sites

    E-commerce & SaaS

    Intelligence artificielle