useDeferredValue es un Hook de React que permite realizar una actualización en diferido de una parte de la UI.

const deferredValue = useDeferredValue(value)

Referencia

useDeferredValue(value)

Llama a useDeferredValue en el nivel superior de tu componente para obtener una versión diferida del valor.

import { useState, useDeferredValue } from 'react';

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

Ver más ejemplos abajo.

Parámetros

  • value: El valor que se quiere diferir. Puede ser de cualquier tipo.

Devuelve

Durante el renderizado inicial, el valor diferido devuelto será el mismo que el valor que se haya proporcionado inicialmente. Durante las actualizaciones, React realizará un primer intento de re-renderizado con el valor anterior (de modo que devolverá el valor anterior) e intentará realizar otro re-renderizado en segundo plano con el nuevo valor (por lo que devolverá el valor actualizado).

Advertencias

  • Los valores que pases a useDeferredValue deben ser tanto valores primitivos (como string y number) u objetos creados fuera del proceso de renderización. Si se crea un nuevo objeto durante el el proceso de renderización e inmediatamente se le pasa a useDeferredValue generará un valor distinto en cada renderizado causando re-renderizados innecesarios en segundo plano.

  • Cuando useDeferredValue recibe un valor diferente (comparado con Object.is), además de la renderización actual (cuando aún utiliza el valor anterior), programa una re-renderización en segundo plano con el nuevo valor. La re-renderización en segundo plano es interrumpible: si hay otra actualización del valor, React reiniciará la re-renderización en segundo plano desde cero. Por ejemplo, si el usuario está escribiendo en un input más rápido de lo que la gráfica que recibe su valor diferido puede volver a renderizarse, la gráfica solo se volverá a renderizar cuando el usuario deje de escribir.

  • useDeferredValue está integrado con <Suspense>. Si la actualización en segundo plano causada por un nuevo valor suspende la UI, el usuario no verá el fallback. Verá el antiguo valor diferido hasta que se carguen los datos.

  • useDeferredValue no previene por sí mismo las peticiones de red adicionales.

  • No existe un retardo fijo causado por useDeferredValue. Tan pronto como React finalice el renderizado original, inmediatamente, empezará a trabajar sobre el re-renderizado en segundo plano con el nuevo valor diferido. Cualquier actualización causada por eventos (como escribir por teclado) interrumpirá y tendrá prioridad respecto al proceso de re-renderizado en segundo plano.

  • El re-renderizado en segundo plano causado por useDeferredValue no dispara Efectos hasta que se haya confirmado en pantalla. Si el proceso de re-renderizado en segundo plano se suspende, los Efectos volverán a ejecutarse una vez los datos hayan sido cargados y la UI se haya actualizado.


Uso

Mostrar contenido desactualizado mientras se carga el contenido actualizado.

Llama a useDeferredValue en el nivel superior de tu componente para retrasar la actualización de alguna parte de tu UI.

import { useState, useDeferredValue } from 'react';

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

Durante el renderizado inicial, el valor diferido será el mismo que el valor que se proporcione.

Durante las actualizaciones, el valor diferido tendrá un «retardo» respecto al último valor. Concretamente, React re-renderizará primero sin actualizar el valor diferido y posteriormente intentará re-renderizar con el nuevo valor recibido en segundo plano.

Analicemos un ejemplo para ver en qué situaciones resulta útil.»

Nota

Este ejemplo asume que se está utilizando una origen de datos con Suspense habilitado:

  • Frameworks para la obtención de datos con Suspense habilitado como Relay y Next.js.

  • Carga diferida de código de componentes con lazy

  • Leer el valor de una promesa con use

  • Componentes que empleen carga diferida mediante lazy.

Aprende más sobre Suspense y sus limitaciones.

En este ejemplo, el componente SearchResults se suspende mientras se obtienen los resultados de búsqueda. Intenta escribir "a", espera a que se muestren los resultados y luego edita el valor a "ab". El resultado para "a" sera reemplazado por el fallback de carga que indica que se están obteniendo los nuevos resultados.

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

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

Una alternativa común en la UI es diferir la actualización de las listas de resultados y seguir mostrando los anteriores resultados hasta que los nuevos estén disponibles. Llama a useDeferredValue para pasar una versión diferida de la query:

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

La query se actualizará inmediatamente, por lo que el input mostrará el nuevo valor. No obstante, el deferredQuery mantendrá el valor previo hasta que los datos se hayan cargado, por lo que SearchResults mostrará resultados obsoletos durante un instante.

Escribe "a" en el siguiente ejemplo, espera a que se carguen los resultados y entonces edita el valor del input a "ab". Observa como, en lugar del Suspense fallback, ahora podrás ver los resultados obsoletos en la lista hasta que los nuevos valores se hayan cargado:

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>
        Buscar álbumes:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={deferredQuery} />
      </Suspense>
    </>
  );
}

Profundizar

¿Cómo funciona realmente diferir un valor?

Puedes pensar que ocurre de acuerdo a estos dos pasos:

  1. En primer lugar, React re-renderiza con la nueva query ("ab") pero utilizando el anterior deferredQuery (cuyo valor aún es "a"). El valor de deferredQuery, el cual se pasa a la lista resultante, está «diferido» respecto al valor de la query.

  2. En segundo plano, React intentará re-renderizar con ambos query y deferredQuery actualizados con el valor "ab". Si este re-renderizado se completa, React lo mostrará por pantalla. De lo contrario, si se «suspende» (los resultados para "ab" aún no se han cargado), React abandonará este intento de renderización y re-intentará este re-renderizado nuevamente una vez los datos hayan sido cargados. El usuario seguirá viendo el valor diferido obsoleto hasta que los datos hayan sido cargados.

La renderización diferida en segundo plano se puede interrumpir. Por ejemplo, si escribimos en el input nuevamente, React abandonará esa renderización y comenzará una nueva con el nuevo valor. React siempre utilizará el último valor proporcionado.

Ten en cuenta que sigue habiendo una petición de red por cada pulsación de tecla. Lo que se aplaza aquí es la visualización de los resultados (hasta que estén listos), no las peticiones de red en sí. Incluso si el usuario continúa escribiendo, las respuestas para cada pulsación de tecla se almacenan en caché, por lo que pulsar Backspace es instantáneo y no se obtiene de nuevo.


Indicar que el contenido es obsoleto

En el ejemplo anterior no se está indicando que los resultados de la lista, para la ultima query ejecutada, aún están cargando. Esto puede llegar a ser confuso para el usuario si los nuevos resultados toman un tiempo para cargarse y estar disponibles. Para hacer esto algo mas obvio para el usuario, se puede añadir una indicación visual cuando los resultados de la lista que se muestra están obsoletos:

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

Con este cambio, tan pronto como el usuario comience a escribir, los resultados obsoletos de la lista se atenuarán temporalmente hasta que los nuevos resultados estén disponibles. También puedes emplear una transición mediante CSS para crear un retardo a la hora de atenuar los resultados de tal forma que se produzca una transición suave y gradual cuando estos se atenúen. Observa el siguiente ejemplo:

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>
        Buscar álbumes:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</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>
    </>
  );
}


Diferir el re-renderizando una parte de la UI

Puedes utilizar useDeferredValue como medio para optimizar el rendimiento. Es útil cuando una parte de tu UI es más lenta a la hora de re-renderizar y no existe una forma fácil de optimizarlo a fin de evitar que otras partes de la UI se bloqueen.

Imagina que tienes un campo de texto input y un componente (como un gráfico o una lista larga) que se vuelve a mostrar cada vez que pulsas una tecla:

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

En primer lugar, optimizaremos SlowList para evitar re-renderizados cuando las props son las mismas. Para hacer esto, lo envolveremos en memo

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

No obstante, esto solo serviría si las props de SlowList son las mismas que en el anterior renderizado. El problema que se experimenta ahora es que el componente es lento cuando las props son distintas y cuando se necesita mostrar valores distintos a los previos.

En concreto, el principal problema de rendimiento es que, cada vez que se introduce un nuevo valor en el input, el componente SlowList recibe nuevas props y se re-renderiza por completo. Esto hace que el comportamiento del componente se sienta entrecortado. En este caso, useDeferredValue te permite priorizar la actualización del input (que es más rápida) frente a la actualización de la lista de resultados (que es más lenta):

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

Esto no hace que el re-renderizado de SlowList sea más rápido. Sin embargo indica a React que el re-renderizado de la lista puede se postergado para que no bloquee la introducción de nuevos valores al input. La actualización de la lista tendrá un retardo con respecto al nuevo valor introducido en el input y posteriormente se actualizará. Tal y como ocurría anteriormente, React intentará actualizar los resultados de la lista lo antes posible, pero no bloqueando al usuario de introducir nuevos valores en el input.

Diferencias entre el uso de useDeferredValue y un re-renderizado no optimizado

Ejemplo 1 de 2:
Re-renderizado diferido de la lista

En este ejemplo, cada ítem del componente SlowList está ralentizado artificialmente para que puedas observar como useDeferredValue te permite mantener el input con una respuesta rápida. Escribe en el input y nota como la escritura se siente rápida mientras la lista tiene un retardo respecto a la escritura.

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

Atención

Esta optimización requiere que SlowList esté envuelto en memo. Esto es porque, cada vez que se produce un cambio en text, React necesita ser capaz de re-renderizar el componente padre rápidamente. Durante este re-renderizado deferredText aún mantiene el valor previo, por lo que SlowList es capaz de saltarse el re-renderizado (las props no han cambiado). Sin utilizar memo, esto podría desencadenar un nuevo re-renderizado, lo cual no cumpliría el propósito de la optimización.

Profundizar

¿Qué diferencia «diferir un valor» respecto a hacer debounce y throttle?

Existen dos técnicas de optimización que podrías haber utilizado en esta situación:

  • Debounce: esperar hasta que el usuario deje de escribir (durante, por ejemplo, un segundo) y actualizar la lista posteriormente.
  • Throttle: Actualizar la lista un numero limitado de veces cada cierto tiempo (por ejemplo, como mucho, una vez por segundo).

Mientras que estas técnicas son útiles en algunos casos, useDeferredValue es mejor en cuanto a optimizar el proceso de renderizado ya que esta profundamente integrado con React y se adapta al dispositivo que utilice el usuario.

En lugar de debounce o throttle, no requiere emplear un retardo fijo. Si el dispositivo del usuario es rápido (por ejemplo una computadora potente), el re-renderizado diferido ocurrirá prácticamente de forma inmediata e imperceptible. Si el dispositivo del usuario es lento la actualización de la lista tras modificar el valor del input tendrá un retardo proporcional a lo lento que sea dicho dispositivo.

Añadir que los re-renderizados diferidos realizados por useDeferredValue se pueden interrumpir por defecto. Esto significa que, si React se encuentra en mitad de un proceso de re-renderizado de una lista con muchos resultados, pero el usuario pulsa una tecla sobre el input, a fin de introducir un nuevo valor, React abandonará ese re-renderizado y comenzará uno nuevo en segundo plano. En contraste, debounce y throttle producen una experiencia entrecortada ya que bloquean y posponen el momento en el que se re-renderiza el contenido por cada pulsación de tecla cuando se introducen nuevos valores en el input.

Si la optimización no ocurre durante el renderizado, debounce y throttle aún son útiles en ese caso. Por ejemplo, te permitirán realizar menos peticiones de red. También puedes utilizar estas técnicas al mismo tiempo.