30 septembre 2025
Activity, le nouveau composant React

4 minutes de lecture

Fraichement sorti sur le canal canary de React, le composant Activity est une nouveauté ayant pour but de faciliter la gestion d'affichage conditionnel de composants, permettant de conserver leur état tout en les cachant visuellement. Découvrons ensemble son utilisation.
Installation
Créons un simple projet React avec Vite :
bun create vite@latest
Nous devrons modifier le package.json
afin de faire pointer les versions de React et React DOM vers la version canary.
Cas d'utilisation
Nous allons réaliser un cas d'utilisation qui peut s'avérer fréquent : la gestion d'un formulaire en plusieurs étapes.
Utilisation basique
Actuellement, l'utilisation la plus classique serait la suivante :
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"
/>
); };
En l'état, ce bout de code est fonctionnel mais présente un inconvénient en terme d'UX : lorsque l'on remplit un champ puis que l'on change d'étape, l'état de ce champ est perdu. C'est logique : notre composant a été démonté. Pour palier à ça, on pourrait tout à fait modifier le code afin de cacher le composant, de sorte qu'il soit toujours présent dans l'arbre React.
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>
);
}
C'est fonctionnel mais pas idéal, imaginons que nos composants doivent gérer des événements, ou du contenu interactif comme une vidéo, nous devrions quand même passer une prop afin de gérer leur activation / désactivation. De même, la présence d'un effect (useEffect
ou useLayoutEffect
) devrait avoir son déclenchement réglé selon cette prop. Bien que cela soit possible, cela rajouterait de la complexité à notre composant.
Le composant Activity à la rescousse
C'est dans ce cadre qu'intervient le composant Activity
. Son rôle est simple : cacher le noeud du DOM avec un display: none
, conserver l'état du noeud React, et n'exécuter les effects uniquement quand notre composant est visible. En quelque sorte, c'est comme si notre composant était partiellement monté et démonté.
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>
);
}
La saisie de texte est maintenant conservée entre chaque changement d'étape. En inspectant le DOM, on peut voir qu'un style display: none
est appliqué à notre input caché.
Pour aller plus loin, on peut observer le comportement des effects, à l'aide de simples console.log
.
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"
/>
);
};
En rafraichissant la page, on peut voir qu'aucun des logs ne s'affiche. Pourtant, notre input est bien dans le DOM. En cliquant sur notre étape 2, nos effects sont bien déclenchés, et les fonctions de cleanup sont exécutées si l'on revient à l'étape 1.
Pré-rendu et données distantes
Afin de récupérer des données distantes, un cas d'utilisation courant serait d'effectuer une requête dans un useEffect
. Or on a vu précédemment que les effects ne sont pas exécutés dans le cadre de l'utilisation d'Activity
. Pour palier à cela, nous allons utiliser le hook use
.
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"
/>
);
};
Maintenant, au chargement de la page, notre promesse fetchAddress
est exécutée dès lors que Step2
est instancié, et on retrouve la valeur résolue affichée dans notre input.
Conclusion
Ce tout nouveau composant Activity
va simplifier certains cas complexes que l'on pouvait rencontrer jusqu'à présent dans nos applications. Son utilisation est très intuitive, mais nécessitera toutefois un peu de refactorisation dans les projets existants, notamment par rapport à la modification du comportement des effects.
Une idée de projet React, besoin d'accompagnement ? N'hésitez pas à nous contacter !
Ressources: