frontend / react
Rendu concurrent : useTransition et useDeferredValue
Explication
Ce que vous allez apprendre
- Comprendre pourquoi un calcul lourd peut faire sembler l'interface figée
- Marquer une mise à jour de state comme non urgente avec
useTransition - Afficher un indicateur de chargement via le booléen
isPending - Retarder une valeur (state ou prop) avec
useDeferredValue - Distinguer clairement ce que ces hooks résolvent de ce qu'ils NE résolvent pas (la vitesse du calcul lui-même)
Dans quel contexte ?
Sur src/pages/CatalogPage.jsx, taper dans le champ de recherche qui filtre 10 000 produits provoque un délai visible avant que chaque caractère n'apparaisse à l'écran : le filtrage lourd bloque le thread principal pendant la frappe. Envelopper le setState qui déclenche ce filtrage dans startTransition permet à React de garder la saisie parfaitement fluide, en reportant le calcul du filtrage tant que l'utilisateur continue de taper.
Étape 1 : le problème concret
Sans précaution, un calcul lourd déclenché par un changement de state (filtrer une longue liste, par exemple) bloque le fil d'exécution JavaScript pendant qu'il tourne. Pendant ce court instant, même taper dans un champ de recherche peut sembler figé, car le navigateur ne peut pas traiter la frappe tant que ce calcul n'est pas terminé.
Étape 2 : ce que permet le rendu concurrent
Le rendu concurrent, rendu possible par l'architecture Fiber vue dans une leçon précédente, offre une porte de sortie. React peut interrompre un rendu jugé non urgent pour laisser passer une interaction plus prioritaire, comme l'affichage du texte tapé.
Étape 3 : marquer une mise à jour comme non urgente
C'est exactement le rôle de useTransition. Il permet d'envelopper un setState précis dans une fonction startTransition, pour dire à React "celui-ci peut attendre, ne bloque jamais l'interface pour lui".
Étape 4 : un indicateur visuel presque gratuit
Une fois cette transition démarrée, le hook retourne un booléen isPending, qui reste vrai tant que le rendu différé n'est pas terminé. On peut s'en servir pour afficher un indicateur discret, sans gérer cet état à la main.
Étape 5 : la même idée, appliquée à une valeur
useDeferredValue répond au même besoin sous un angle différent : au lieu d'envelopper une action, il retarde une VALEUR précise. Elle reste "en retard" pendant un rendu coûteux, puis se rattrape une fois celui-ci terminé.
Étape 6 : pourquoi cette variante est parfois plus pratique
Son avantage apparaît quand on ne contrôle pas directement le setState d'origine, par exemple une valeur reçue en prop depuis un composant qu'on ne veut pas modifier. useTransition, lui, exige d'envelopper l'appel au setter lui-même.
Le piège à ne jamais oublier
Point crucial avant de conclure : ni l'un ni l'autre n'accélère réellement un calcul coûteux. Ils se contentent de réorganiser QUAND ce calcul a lieu, pour garder l'interface réactive pendant qu'il tourne en arrière-plan — la vraie performance du calcul reste l'affaire de useMemo.
Piège fréquent
Envelopper un setState dans startTransition en espérant que le filtrage de 10 000 produits devienne plus rapide est une erreur de compréhension : le calcul prend exactement le même temps. Ce qui change, c'est que l'interface reste réactive PENDANT ce calcul, pas la durée du calcul lui-même.
| Hook | Ce qu'il enveloppe | Quand le préférer |
|---|---|---|
useTransition | Une action (setState précis) | Le setState d'origine est sous ton contrôle direct |
useDeferredValue | Une valeur (state ou prop) | La valeur vient d'ailleurs (prop, contexte externe) |
Et ensuite ?
Une fois ce mécanisme de priorité compris, la prochaine étape revient sur les refs, avec les nouveautés apportées par React 19.
Commandes & code
Rendu concurrent : useTransition et useDeferredValue
Garder une interface réactive pendant qu'un rendu coûteux se calcule en arrière-plan, sans bloquer la saisie.
import { useState, useTransition } from "react";
function Recherche() {
const [terme, setTerme] = useState("");
const [resultats, setResultats] = useState([]);
const [enTransition, startTransition] = useTransition();
// isPending : true tant que le rendu marqué "transition" n'a pas fini -- pilotable dans le JSX
function gererChangement(e) {
const valeur = e.target.value;
setTerme(valeur); // urgent : l'input doit rester réactif à chaque frappe
startTransition(() => {
// non urgent, interruptible : si l'utilisateur retape avant la fin, React abandonne
// ce calcul en cours et repart sur la nouvelle valeur, sans jamais bloquer l'input
setResultats(rechercheLourde(valeur));
});
}
return (
<div>
<input value={terme} onChange={gererChangement} />
{enTransition && <span className="spinner-discret" />}
<ListeResultats resultats={resultats} />
</div>
);
}import { useState, useDeferredValue, useMemo } from "react";
// useDeferredValue : variante déclarative de la même idée, sans wrapper de fonction explicite
function RechercheDeferee() {
const [terme, setTerme] = useState("");
const termeDiffere = useDeferredValue(terme);
// termeDiffere "traîne" derrière terme pendant un rendu coûteux, se rattrape ensuite
const resultats = useMemo(
() => rechercheLourde(termeDiffere),
[termeDiffere] // recalculé seulement quand la valeur DIFFÉRÉE change, pas à chaque frappe
);
const enRetard = terme !== termeDiffere; // signal utile pour un état visuel "obsolète"
return (
<div>
<input value={terme} onChange={(e) => setTerme(e.target.value)} />
<div style={{ opacity: enRetard ? 0.6 : 1 }}>
<ListeResultats resultats={resultats} />
</div>
</div>
);
}// Différence clé : useTransition entoure une ACTION (un setState précis à différer),
// useDeferredValue entoure une VALEUR (peu importe d'où elle vient, prop ou state)
function ComparaisonUsage({ propExterne }) {
// useDeferredValue fonctionne même sur une prop reçue, pas seulement un state local
const propDifferee = useDeferredValue(propExterne);
return <RenduCouteux valeur={propDifferee} />;
}// Combiné à memo() : évite de refaire le rendu coûteux tant que la valeur différée n'a pas changé
const ListeResultats = memo(function ListeResultats({ resultats }) {
return <ul>{resultats.map((r) => <li key={r.id}>{r.titre}</li>)}</ul>;
});Résumé
useTransitionmarque unsetStateprécis comme non urgent : React peut l'interrompre pour une interaction plus prioritaire.isPendingreflète l'état de la transition en cours, exploitable directement dans le JSX (spinner discret).useDeferredValueretarde une VALEUR (state ou prop) plutôt qu'une action, utile quand on ne contrôle pas le setter.- Ces deux hooks n'accélèrent rien : ils reportent un rendu coûteux pour garder l'UI réactive pendant son calcul.
Exercices pratiques
Mission : rendre CatalogPage réactive sans mentir sur la vitesse du filtrage
Objectif : Corriger une saisie qui semble figée sur un catalogue de 10 000 produits, en choisissant le bon hook de rendu concurrent selon qui possède le state filtré.
Contexte
Sur CatalogPage.jsx, taper dans le champ de recherche provoque un délai visible avant que chaque caractère n'apparaisse : le filtrage de 10 000 produits bloque le thread principal. Un premier développeur enveloppe setResultats(filtrerLourd(valeur)) dans startTransition et annonce fièrement que "le filtrage est maintenant deux fois plus rapide". Un second cas se présente ensuite : un composant CatalogueExterne reçoit termeRecherche en PROP depuis un parent que l'équipe ne contrôle pas, et doit lui aussi éviter de bloquer l'UI pendant son filtrage.