useDeferredValue
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);
// ...
}
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 (comostring
ynumber
) 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 auseDeferredValue
generará un valor distinto en cada renderizado causando re-renderizados innecesarios en segundo plano. -
Cuando
useDeferredValue
recibe un valor diferente (comparado conObject.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 delvalor
, React reiniciará la re-renderización en segundo plano desde cero. Por ejemplo, si el usuario está escribiendo en uninput
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á elfallback
. 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.»
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
Puedes pensar que ocurre de acuerdo a estos dos pasos:
-
En primer lugar, React re-renderiza con la nueva
query
("ab"
) pero utilizando el anteriordeferredQuery
(cuyo valor aún es"a")
. El valor dedeferredQuery
, el cual se pasa a la lista resultante, está «diferido» respecto al valor de laquery
. -
En segundo plano, React intentará re-renderizar con ambos
query
ydeferredQuery
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.
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} /> </> ); }
Profundizar
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.