useDeferredValue est un Hook React qui vous laisse différer la mise à jour d’une partie de l’interface utilisateur (UI, NdT).

const deferredValue = useDeferredValue(value)

Référence

useDeferredValue(value)

Appelez useDeferredValue à la racine de votre composant pour recevoir une version différée de cette valeur.

import { useState, useDeferredValue } from 'react';

function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}

Voir d’autres exemples ci-dessous.

Paramètres

  • value : la valeur que vous souhaitez différer. Elle peut être de n’importe quel type.

Valeur renvoyée

Durant le rendu initial, la valeur différée renvoyée sera celle que vous avez fournie. Lors des mises à jour, React tentera d’abord un rendu avec l’ancienne valeur (il va donc renvoyer l’ancienne valeur), et ensuite essayer en arrière-plan un rendu avec la nouvelle valeur (il va donc renvoyer la valeur à jour).

Limitations

  • Les valeurs que vous passez à useDeferredValue doivent être soit des valeurs primitives (comme des chaînes de caractères ou des nombres), soit des objets créés en-dehors du rendu. Si vous créez un nouvel objet pendant le rendu et que vous le passez immédiatement à useDeferredValue, il sera différent à chaque rendu, entraînant des rendus inutiles en arrière-plan.

  • Quand useDeferredValue reçoit une valeur différente (en comparant au moyen de Object.is), en plus du rendu en cours (dans lequel il utilisera encore la valeur précédente), il planifie un rendu supplémentaire en arrière-plan avec la nouvelle valeur. Ce rendu d’arrière-plan est susceptible d’être interrompu : s’il y a un nouvelle mise à jour de value, React le recommencera de zéro. Par exemple, si l’utilisateur tape dans un champ de saisie trop rapidement pour qu’un graphique basé sur sa valeur différée puisse suivre, le graphique ne se mettra à jour qu’une fois que l’utilisateur aura terminé sa saisie.

  • useDeferredValue s’intègre très bien avec <Suspense>. Si la mise à jour d’arrière-plan suspend l’UI, l’utilisateur ne verra pas l’UI de secours : il continuera à voir l’ancienne valeur différée jusqu’à ce que les données soient chargées.

  • useDeferredValue n’empêche pas par lui-même des requêtes réseau supplémentaires.

  • useDeferredValue ne recourt pas à un différé de durée fixe. Dès que React termine le premier nouveau rendu, il commence immédiatement à travailler sur le rendu d’arrière-plan avec la nouvelle valeur différée. Toute mise à jour causée par des évènements (comme écrire dans un champ de saisie) interrompra le rendu d’arrière-plan et sera traitée en priorité.

  • Le rendu d’arrière-plan entraîné par un useDeferredValue ne déclenche pas les Effets tant qu’il n’est pas retranscrit à l’écran. Si le rendu d’arrière-plan suspend, ses Effets ne seront lancés qu’après que les données seront chargées et que l’UI sera mise à jour.


Utilisation

Afficher du contenu obsolète pendant le chargement du nouveau contenu

Appelez useDeferredValue à la racine de votre composant pour différer la mise à jour de certaines parties de votre interface utilisateur.

import { useState, useDeferredValue } from 'react';

function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}

Lors du rendu initial, la valeur différée sera la même que la valeur que vous avez fournie.

Lors des mises à jour, la valeur différée sera « en retard » par rapport à la dernière valeur. Plus particulièrement, React fera d’abord un rendu sans mettre à jour la valeur différée, puis tentera un rendu supplémentaire en arrière-plan avec la nouvelle valeur reçue.

Parcourons un exemple afin de comprendre l’utilité de ce Hook.

Remarque

Cet exemple part du principe que vous utilisez une source de donnée compatible avec Suspense :

  • Le chargement de données fourni par des frameworks intégrant Suspense tels que Relay et Next.js
  • Le chargement à la demande de composants avec lazy
  • La lecture de la valeur d’une promesse avec use

Apprenez-en davantage sur Suspense et ses limitations.

Dans cet exemple, le composant SearchResults suspend pendant le chargement des résultats de recherche. Essayez de saisir "a", attendez que les résultats s’affichent, puis modifiez la saisie en "ab". Les résultats pour "a" sont remplacés par une UI de secours pendant le chargement.

import { Suspense, useState } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  return (
    <>
      <label>
        Rechercher des albums :
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Chargement...</h2>}>
        <SearchResults query={query} />
      </Suspense>
    </>
  );
}

Une alternative visuelle courante consiste à différer la mise à jour d’une liste de résultats, en continuant à montrer les anciens résultats jusqu’à ce que les nouveaux soient prêts. Appelez useDeferredValue pour pouvoir passer une version différée de la recherche :

export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Rechercher des albums :
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Chargement...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}

La query va se mettre à jour immédiatement, donc le champ de saisie affichera la nouvelle valeur. En revanche, la deferredQuery gardera son ancienne valeur jusqu’à ce que les données soient chargées, et SearchResults affichera les anciens résultats dans l’intervalle.

Tapez"a" dans l’exemple ci-dessous, attendez que les résultats soient chargés, et modifiez ensuite votre saisie pour "ab". Remarquez qu’au lieu d’apercevoir l’interface de chargement, vous continuez à voir la liste des anciens résultats jusqu’à ce que les nouveaux résultats soient chargés :

import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  return (
    <>
      <label>
        Rechercher des albums :
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Chargement...</h2>}>
        <SearchResults query={deferredQuery} />
      </Suspense>
    </>
  );
}

En détail

Comment une valeur différée fonctionne-t-elle sous le capot ?

Imaginez un déroulement en deux étapes :

  1. Pour commencer, React refait un rendu avec la nouvelle query ("ab") mais avec l’ancienne deferredQuery (toujours "a"). La valeur deferredQuery, que vous passez à la liste de résultats, est différée : elle est « en retard » par rapport à la valeur query.

  2. En arrière-plan, React tente alors un autre rendu avec query et deferredQuery valant toutes les deux "ab". Si ce rendu aboutit, React l’affichera à l’écran. Cependant, s’il suspend (les résultats pour "ab" ne sont pas encore chargés), React abandonnera cet essai de rendu, et essaiera à nouveau une fois les données chargées. L’utilisateur continuera à voir l’ancienne valeur différée jusqu’à ce que les données soient prêtes.

Le rendu différé « d’arrière-plan » est susceptible d’être interrompu. Par exemple, si vous tapez à nouveau dans le champ de saisie, React l’abandonnera et recommencera avec la nouvelle valeur. React utilisera toujours la dernière valeur fournie.

Remarquez qu’il y a quand même une requête réseau par frappe clavier. Ce qui est différé ici, c’est l’affichage des résultats (jusqu’à ce qu’ils soient prêts), et non pas les requêtes réseau elles-mêmes. Même si l’utilisateur continue à saisir, les réponses pour chaque frappe clavier sont mises en cache, donc les données ne sont pas chargées à nouveau lorsqu’on appuie sur Backspace : la mise à jour est alors instantanée.


Indiquer que le contenu est obsolète

Dans l’exemple ci-avant, il n’y aucune indication que la liste des résultats pour la dernière requête est toujours en train de charger. Cela peut être déroutant pour l’utilisateur si les nouveaux résultats prennent du temps à charger. Afin de bien signifier que la liste de résultats ne reflète pas encore la dernière recherche, vous pouvez ajouter une indication visuelle lorsque l’ancienne liste de résultats est affichée :

<div style={{
opacity: query !== deferredQuery ? 0.5 : 1,
}}>
<SearchResults query={deferredQuery} />
</div>

Avec ce changement, dès que vous commencerez à taper, l’ancienne liste de résultats sera légèrement assombrie, jusqu’à ce que la nouvelle liste de résultats soit chargée. Vous pouvez également ajouter une transition CSS pour un résultat plus graduel, comme dans l’exemple ci-dessous :

import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
  return (
    <>
      <label>
        Rechercher des albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Chargement...</h2>}>
        <div style={{
          opacity: isStale ? 0.5 : 1,
          transition: isStale ? 'opacity 0.2s 0.2s linear' : 'opacity 0s 0s linear'
        }}>
          <SearchResults query={deferredQuery} />
        </div>
      </Suspense>
    </>
  );
}


Différer le rendu d’une partie de l’UI

Vous pouvez également utiliser useDeferredValue pour optimiser les performances. C’est pratique lorsqu’une partie de votre UI a un rendu lent, qu’il n’y a pas de manière simple de l’optimiser, et que vous voulez éviter qu’elle bloque le reste de l’UI.

Imaginez que vous avez un champ textuel et un composant (comme un graphique ou une longue liste) qui refait son rendu à chaque frappe clavier :

function App() {
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={text} />
</>
);
}

Pour commencer, optimisez SlowList pour éviter un nouveau rendu quand ses propriétés n’ont pas changé. Pour ce faire, enrobez-le avec memo :

const SlowList = memo(function SlowList({ text }) {
// ...
});

Cependant, ça ne vous aide que si les propriétés de SlowList sont les mêmes que lors du rendu précédent. Ce composant peut toujours être lent lorsque les propriétés sont différentes, et que vous avez effectivement besoin de produire un rendu visuel distinct.

Concrètement, le souci de performances principal vient de ce que lorsque vous tapez dans le champ de saisie, la SlowList reçoit des nouvelles propriétés, et la lenteur de sa mise à jour rend la saisie saccadée. Dans un tel cas, useDeferredValue vous permet de prioriser la mise à jour du champ de saisie (qui doit être rapide) par rapport à celle de la liste de résultats (qui peut être plus lente) :

function App() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={deferredText} />
</>
);
}

Ça n’accélère pas le rendu de SlowList. Néanmoins, ça indique à React de déprioriser le rendu de la liste afin de ne pas bloquer les frappes clavier. La liste sera « en retard » par rapport au champ de saisie, pour finalement le « rattraper ». Comme auparavant, React essaiera de mettre à jour la liste le plus vite possible, mais sans empêcher l’utilisateur de taper.

Différence entre useDeferredValue et un rendu non optimisé

Exemple 1 sur 2 ·
Différer le rendu de la liste

Dans cet exemple, chaque élément du composant SlowList est artificiellement ralenti afin que vous puissiez constater que useDeferredValue permet de garder le champ de saisie réactif. Écrivez dans le champ de saisie et voyez comme la saisie reste réactive, alors que la liste « est en retard ».

import { useState, useDeferredValue } from 'react';
import SlowList from './SlowList.js';

export default function App() {
  const [text, setText] = useState('');
  const deferredText = useDeferredValue(text);
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <SlowList text={deferredText} />
    </>
  );
}

Piège

Cette optimisation nécessite que SlowList soit enrobée par un memo. En effet, à chaque fois que text change, React doit pouvoir refaire le rendu du composant parent rapidement. Durant ce rendu, deferredText a toujours sa valeur précédente, donc SlowList peut s’épargner un nouveau rendu (ses propriétés n’ont pas changé). Sans memo, il y aurait un nouveau rendu dans tous les cas, ce qui tue tout l’intérêt de l’optimisation.

En détail

Valeur différée, debouncing et throttling : quelles différences ?

Il existe deux technique d’optimisation courantes que vous avez peut-être utilisées auparavant dans ce genre de scénarios :

  • Le debouncing signifie que vous attendriez que l’utilisateur cesse de taper (par exemple pendant une seconde) avant de mettre à jour la liste.
  • Le throttling signifie que vous ne mettriez à jour la liste qu’à une fréquence limitée (par exemple au maximum une fois par seconde).

Mêmes si ces techniques sont utiles dans certains cas, useDeferredValue est plus adapté pour optimiser le rendu, car il est totalement intégré avec React et il s’adapte à l’appareil de l’utilisateur.

Contrairement au debouncing et au throttling, il ne nécessite pas de choisir un délai fixe. Si l’appareil de l’utilisateur est rapide (par exemple un ordinateur puissant), le rendu différé serait quasiment immédiat, le rendant imperceptible pour l’utilisateur. Si l’appareil est lent, la liste serait « en retard » par rapport au champ de saisie, proportionnellement à la lenteur de l’appareil.

De plus, les rendus différés planifiés par useDeferredValue sont par défaut susceptibles d’être interrompus, ce qui n’est pas le cas du debouncing ou du throttling. Ça signifie que si React est en plein milieu du rendu d’une vaste liste, et que l’utilisateur ajuste sa saisie, React abandonnera ce rendu, traitera la frappe, et recommencera le rendu en arrière-plan. Par opposition, le debouncing et le throttling donneraient ici toujours une expérience saccadée car ils sont bloquants : ils diffèrent simplement le moment auquel le rendu bloque la frappe.

Si vous souhaitez optimiser des traitements hors du rendu, le debouncing et le throttling restent utiles. Par exemple, ils peuvent vous permettre de lancer moins de de requêtes réseau. Vous pouvez parfaitement combiner ces techniques.