Menu

Le hook useTransition de React : une interface réactive

useTransition marque une mise à jour d'état comme non urgente, pour que React garde la page réactive pendant qu'il fait le rendu de la partie lente en arrière-plan. Découvrez isPending, startTransition, les transitions asynchrones de React 19, et pourquoi ce n'est pas du debounce.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

useTransition vous permet de marquer une mise à jour d'état comme non urgente. React fait le rendu de cette mise à jour en arrière-plan et l'abandonne ou la recommence si quelque chose de plus urgent arrive, comme un clic ou une frappe, pour que la page reste réactive pendant qu'une partie lente de l'écran rattrape son retard.

Cliquez sur posts puis, avant que la liste apparaisse, cliquez sur contact. Contact s'affiche immédiatement : React a jeté le rendu de posts à moitié terminé. Remplacez maintenant startTransition(() => { setTab(next); }) par un simple setTab(next); et refaites la même chose : la page se fige environ 300 ms après le clic sur posts, et votre clic sur contact attend que posts soit terminé.

La syntaxe

const [isPending, startTransition] = useTransition();

startTransition(() => {
    setTab(next);
});
  • startTransition prend une fonction et l'exécute tout de suite. Toutes les mises à jour d'état qu'elle contient sont marquées comme transitions.
  • isPending vaut true depuis le démarrage de la transition jusqu'à ce que React ait fini son rendu et commité le résultat.

useTransition ne prend aucun argument. La fonction que vous passez s'exécute de façon synchrone ; la transition concerne la façon dont React fait le rendu de la mise à jour qui en résulte, pas le moment où votre fonction s'exécute.

Ce que signifie « non urgent »

React distingue deux types de mises à jour. Les mises à jour urgentes, comme taper, cliquer et appuyer, doivent se refléter immédiatement, sinon la page semble cassée. Les mises à jour de transition, comme passer à une nouvelle vue ou afficher des résultats filtrés, peuvent prendre un moment.

React fait le rendu d'une transition par petites tranches et vérifie entre les composants si quelque chose d'urgent est arrivé. Si c'est le cas, React met en pause ou abandonne la transition, traite la mise à jour urgente, puis recommence la transition avec l'état le plus récent. Pendant le rendu d'une transition, l'écran précédent reste affiché ; React ne montre pas un résultat à moitié construit. C'est pourquoi l'exemple continue d'afficher le texte about, avec l'indication Loading à côté des boutons, jusqu'à ce que les posts soient prêts.

L'interruption a lieu entre les composants. Un seul composant qui bloque pendant 300 ms ne peut pas être coupé en deux, c'est pourquoi le travail lent ci-dessus est réparti sur 300 petits composants SlowPost.

Afficher une indication d'attente

isPending est l'endroit idéal pour un indicateur de chargement qui ne remplace pas le contenu actuel : un spinner à côté du bouton, un texte grisé, un libellé « Enregistrement... ». Dans le premier exemple, il affiche Loading... à côté des onglets. Vous pouvez aussi l'utiliser pour styliser le contenu :

<div style={{ opacity: isPending ? 0.6 : 1 }}>{content}</div>

Filtrer une grande liste pendant la saisie

Vous ne pouvez pas placer la valeur d'un champ lui-même dans une transition. Un <input> contrôlé doit recevoir sa nouvelle valeur dans la même mise à jour urgente, sinon des caractères sont perdus. Gardez plutôt deux éléments d'état : le texte du champ (urgent) et la requête utilisée par la liste (une transition).

Tapez 123 rapidement. Le champ ne prend jamais de retard. La console affiche quand même une ligne rendering results par frappe, car Results écrit dès que React commence son rendu, mais les listes pour 1 et 12 n'arrivent jamais à l'écran : chaque frappe plus récente a interrompu le rendu en cours, et React a recommencé avec la dernière requête. Results est volontairement enveloppé dans memo : sans lui, le rendu urgent de chaque frappe ferait aussi le rendu de la liste lente avec l'ancienne requête, et vous perdriez le bénéfice.

startTransition sans le hook

react exporte aussi startTransition comme simple fonction. Elle marque les mises à jour comme transitions, exactement comme le hook, mais vous n'obtenez pas isPending. Utilisez-la là où vous ne pouvez pas appeler de hook, par exemple dans une bibliothèque de données ou un routeur hors de tout composant.

import { startTransition } from 'react';

function navigate(url) {
    startTransition(() => {
        setCurrentUrl(url);
    });
}

Transitions asynchrones dans React 19

Depuis React 19, la fonction que vous passez peut être async. isPending reste alors à true pendant toute l'exécution de la fonction, attente comprise, ce qui en fait un moyen simple d'afficher un état d'attente pendant une requête. Le faux saveName ci-dessous remplace une vraie requête.

Notez le second startTransition autour de setSaved. React perd la trace de la transition après un await, donc une mise à jour d'état qui suit doit être enveloppée à nouveau pour faire partie de la transition. React appelle une fonction passée de cette façon une action ; useActionState et les actions de formulaire reposent sur le même mécanisme.

Une transition n'est pas un debounce

Un debounce attend un temps fixe après la dernière frappe avant de faire quoi que ce soit. Une transition n'attend pas. Elle commence le rendu immédiatement, et si une mise à jour plus récente arrive, elle abandonne l'ancien travail. Sur un appareil rapide, le résultat apparaît sans aucun délai ; sur un appareil lent, React en fait autant que possible entre les frappes. Ce qu'une transition ne peut pas faire, c'est réduire le nombre de requêtes réseau : si chaque frappe lance un fetch, appliquez un debounce au fetch.

Transitions et Suspense

Les transitions changent aussi le comportement d'une frontière Suspense. Normalement, quand un composant à l'intérieur d'une frontière se suspend (un composant lazy qui se charge, ou des données lues avec use), React remplace le contenu par le contenu de repli. Si la mise à jour qui a causé la suspension était une transition, React garde plutôt le contenu déjà à l'écran, et isPending reste à true jusqu'à ce que le nouveau contenu soit prêt. C'est ainsi que les routeurs évitent d'afficher un spinner pleine page à chaque navigation : ils enveloppent la navigation dans une transition, et l'ancienne page reste visible jusqu'à ce que la suivante puisse s'afficher. Un contenu jamais affiché utilise toujours le contenu de repli, donc une frontière nouvellement révélée se comporte normalement.

Ce qui ne fonctionne pas

  • Les mises à jour dans un timeout. startTransition(() => setTimeout(() => setTab(next), 100)) ne marque pas la mise à jour, car elle s'exécute après le retour de startTransition. Placez plutôt startTransition dans le timeout.
  • Un seul composant lent. React ne peut interrompre qu'entre les composants, donc un composant qui bloque longtemps bloque toujours.
  • Contrôler un champ. Comme indiqué plus haut, l'état du champ lui-même reste urgent.

Pour le cas où vous ne possédez pas le setter d'état et recevez seulement une valeur, comme une prop, useDeferredValue vous donne le même comportement.

Questions fréquentes

Que fait useTransition ?

Il vous donne startTransition, qui marque les mises à jour d'état qu'il contient comme non urgentes, et isPending, qui vaut true tant que cette mise à jour est encore en cours de rendu. React peut interrompre un rendu non urgent pour traiter d'abord les clics et la saisie.

Quelle est la différence entre useTransition et startTransition ?

Les deux marquent des mises à jour comme transitions. Le hook useTransition vous donne en plus isPending pour un indicateur de chargement. La fonction startTransition importée seule depuis react fonctionne hors des composants mais n'a pas d'indicateur d'attente.

Peut-on utiliser useTransition pour un champ texte ?

Pas pour la valeur du champ lui-même. Un champ contrôlé doit se mettre à jour de façon synchrone, sinon la saisie se dérègle. Gardez l'état du champ urgent et placez dans une transition la mise à jour coûteuse qui en dépend, ou utilisez useDeferredValue.

useTransition, est-ce la même chose qu'un debounce ?

Non. Un debounce attend un temps fixe avant de faire quoi que ce soit. Une transition commence le rendu tout de suite et est interrompue si une mise à jour plus récente arrive, donc sur un appareil rapide le résultat apparaît sans délai.

startTransition peut-il prendre une fonction async ?

Oui, depuis React 19. isPending reste à true jusqu'à la fin de la fonction async. Les mises à jour d'état après un await doivent être enveloppées dans un autre appel à startTransition pour être marquées comme transition.

Illustration des langages de programmation de Coddy

Apprendre à coder avec Coddy

COMMENCER