useDeferredValue
Diferir el valor de una variable de estado en un componente funcional en React 18

Desarrollando código desde los 80s desde que en los 80 vi por primera vez un Amstrad en un escaparate de una tienda de electrónica en mi ciudad natal, lo que me llevó a insistirles a mis padres para que me apuntaran a clases de informática donde aprendí cosas como Basic y ensamblador del x86.
A lo largo de mi carrera profesional he pasado por varias revoluciones (y crisis) que han hecho que me haya tenido que ir reinventando para poder seguir desarrollando mi trabajo de la mejor forma posible.
Creo que el desarrollo de software tiene una parte de ciencia pero también una de artesanía (y creatividad) que lo convierten en algo apasionante.
Eternamente contagiado con el Síndrome del Impostor.
Supongamos que tenemos un página que está formada por un cuadro de búsqueda en el que podremos ir escribiendo un término a buscar y por una serie de componentes más o menos grande en el que lo único que vamos a hacer es renderizar lo que sea que se escriba en el cuadro de texto:

El problema ante una situación como esta es que "algo" no está yendo bien dentro de la interfaz de usuario puesto que la escritura en los componentes de texto que se encargarán de mostrar lo que estamos buscando no fluye de la forma en la que nos gustaría que lo hiciese haciendo que la interfaz parezca que va a saltos (la intefaz no está respondiendo de forma inmediata a lo que escribimos en el cuadro de texto).
El código de partida del componente que estamos usando es el siguiente donde podemos ver que estamos haciendo uso de un atributo del estado de nuestro componente denominado query donde guardaremos el texto que estamos buscando de tal manera que cuando cambiamos el texto que se escribe en el cuadro de texto actualizaremos su valor y además se le pasará como prop al componente ShowList que es el encargado de mostrar la lista de componentes que muestran el texto por el que estamos realizando la búsqueda.
import { useState } from 'react'
import { SlowList } from './SlowList'
export const Page = () => {
const [query, setQuery] = useState('')
return (
<div className='page'>
<input
onChange={e => setQuery(e.target.value)}
placeholder='Search...'
type='text'
value={query}
/>
<ShowList text={query} />
</div>
)
}
en el código que se encargará de que el texto que se recibe como prop no se renderice de forma automática pese a que en nuestro ejemplo las cosas son muy sencillas para facilitar la explicación.
Pensemos, por ejemplo, que dentro del componente
ShowListse estén haciendo una serie de cálculo complejos cada vez que se muestra el texto que se está buscando lo que hace que el contenido no se renderice de forma inmediata.
Por lo tanto el problema que tenemos viene de que cada vez que tecleamos una letra en el campo de texto estaremos actualizando el atributo del estado query lo que provoca además que se vuelva a renderizar el componente ShowList recibiendo una nueva prop query y teniendo que volver a realizar todos las operaciones lentas.
Como podemos entender en ningún caso nos gustaría tener una aplicación en producción que tuviera unos problemas de performance como los que acabamos de describir.
useDeferredValue
Para solucionar este tipo de problemas React a partir de su versión 18 pone a nuestra disposición el hook useDeferredValue. Vamos a ver cómo utilizarlo para poder solventarlo.
Lo primero, como siempre, será importar el hook:
import { useDeferredValue, useState } from 'react'
Y ahora lo que tenemos que hacer es pasar a utilizarlo sabiendo que como parámetro recibirá el atributo del estado que no queremos que sea actualizado de forma automática sino que se espere un cantidad de tiempo que está predeterminada por React. En nuestro caso, este atributo del estado es query por lo que simplemente escribiremos:
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
Como podemos ver useDeferredValue nos estará retornando un valor que se corresponde con el valor actualizado del atributo del estado query una vez ha pasada la cantidad de tiempo definida por React y, por lo tanto, este ha de ser el valor que le tendremos que pasar a nuestro componente SlowList para que renderice todos los textos:
<ShowList text={deferredQuery} />
Dejaremos el atributo
querydel estado como el valor del campo de texto porque lo que sí que queremos es que las modificaciones que vayamos realizando en este campo se vean reflejadas de forma inmediata en la interfaz.
En la siguiente imagen podemos ver cómo se está comportando ahora nuestra aplicación logrando el efecto que perseguíamos puesto que somos capaces de escribir lo que queremos buscar en el cuadro de texto sin que se produzca ninguna interrupción el flujo de renderizado de nuestra interfaz.

De hecho, si somos lo suficientemente rápidos a la hora de teclear el término a buscar podremos ver que ShowList solamente se renderizará una sola vez lo cual mejora muchísimo el performance de nuestra aplicación.
Tras todos estos cambios el código completo de nuestro componente Page es el siguiente:
import { useDeferredValue, useState } from 'react'
import { SlowList } from './SlowList'
export const Page = () => {
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
return (
<div className='page'>
<input
onChange={e => setQuery(e.target.value)}
placeholder='Search...'
type='text'
value={query}
/>
<ShowList text={deferredQuery} />
</div>
)
}
¿Qué hemos conseguido con la utilización de useDeferredValue?
En primer lugar hemos solucionado el problema de performance que habíamos descrito al principio.
No hemos logrado solucionar los posibles problemas de rendimiento que pueda haber dentro del código de
SlowListpor lo que seguirán ahí presentes y quizás se debería hacer alguna optimización dentro del mismo que ayudase a mejorar la performance.Hemos priorizado qué elementos de la interfaz de usuario son más prioritarios a la hora de trabajar con nuestra aplicación evistando de esta manera problemas derivados de cosas como que la interfaz se quede congelada.
Importate: La forma en la que funcióna
useDeferredValuees sencilla puesto que toma como parámetro un valor del estado (en nuestro casoquery) y lo que hace es retornar un nuevo valor (que nosotros estamos guardando en la variabledeferredQuery) el cual será idéntico aquerycon la única salvedad en que se retornará pasada una cantidad de tiempo.
También tenemos que saber que no tenemos en ningún momento la garantía de que deferredQuery vaya a tener el mismo valor que query en cada uno de los renderizados que se lleva a cabo en la aplicación. De hecho si añadimos un useEffect a nuestro componente en el que escribiremos por la consola los valores de ambas variables como se puede ver a continuación donde además vamos a establece un valor inicial para query puesto que nos ayudará a facilitar la explicación:
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
useEffect(() => {
console.log('Query:', query)
console.log('DeferredQuery:', deferredQuery)
console.log('--- End of render ---')
}, [query, deferredQuery])
Si ahora volvemos a nuestra aplicación y vemos qué ocurre dentro de la consola de JavaScript observaremos como en primer lugar se estará mostrando un mensaje en el que el valor de ambas variables es el mismo (se corresponde con el primer renderizado del componente y en ese instante ambos tienen la cadena vacía como valor).

Importante: a partir de lo que acabamos de ver podemos deducir que siempre el valor del deferredValue va a ser el mismo que el del value sobre el que se apoya en el primer renderizado de nuestro componente.
Vamos a ver ahora qué es lo que sucede en la consola cuando pasamos a escribir en el campo de texto centrando nuestra atención en los mensajes que se muestran en la consola:

Lo primero que tenemos que ver es que en total se han producido dos nuevos renderizados de nuestro componente pese a que nosotros solamente hemos presionado una nueva teclas en el campo de texto una sola vez cuando podríamos estar pensando que únicamente se debería producir un único renderizado. En el primero de estos dos renderizados si nos fijamos en el log que tiene asociado vemos que el valor de query es teste que es exactamente lo que nosotros queremos que sea puesto que se corresponde con lo que está contenido en el campo de búsqueda pero vemos además que deferredQuery sigue teniendo el valor test.
Nota: esto viene a demostrar lo que hemos dicho anteriormente de que gracias al uso de
deferredValueno tenemos en ningún momento la garantía de que el deferred value tenga el mismo valor que el original value excepto en el primer renderizado del componente).
Si lo vemos un poco en más detalle cuando hemos pulsado en el campo de texto la tecla e lo que sucede es que se está lanzando un nuevo render por parte de React como consecuencia de que se haya modificado uno de los atributos de estado de nuestro componente pero además useDeferredValue lo que está haciendo por debeja es una lanzar un proceso de re-render de nuestro componente que se quedará en background y no será ejecutado hasta el momento en el que el valor del deferred value sea el mismo que el del original value y siempre se lanzará este re-render cuando esto así sea.
En nuestro caso esto se traduce en que cuando tecleamos e en el cuadro de texto se lanza el render por la modificación del estado gracias a setQuery pero además se lanza un proceso de re-render en background de tal manera que cada cierto tiempo lo que React hace es establecer el valor que tiene el deferred value y comprobar si es el mismo que el original value en cuyo caso ejecutará el re-render asociado a useDeferredValue que había sido puesto en background.
Nota: de hecho lo que sucede es que este proceso de re-render se trata de llevar a cabo siempre por parte de React pero tiene una característica especial y es que es interrumpible lo que hace que si ambos valores no coincidan no se ejecute y se vuelva a poner en background.
Ahora que sabemos esto ya podemos entender por qué aparece el segundo mensaje en la consola de JavaScript cuando hemos tecleado un nuevo caracter en el cuadro de texto aparece este segundo re-render mostrando como el valor de query y de deferredQuery es el mismo.
¿Qué sucederá ahora si tecleamos dos teclas de forma rápida en el campo de búsqueda? Pues como podemos ver en la siguiente imagen estaremos antes cuatro renders:
Tenemos el render inicial donde el original value y el deferred value es el mismo.
Segundo render que se corresponde con la pulsación de la primera tecla dentro del cuadro de búsqueda donde
querytiene el valortesteydeferredQueryel valortest.Tercer render que se corresponde con la pulsación de segunda tecla donde
querytiene el valortesteeydeferredValueel valortest.Último render que se corresponde con al reconciliación entre los valores de
queryy dedefaultQueryya que ambas variables ahora pasan a tener el valortestee.

Recordemos que los re-renders que se lanzan como consecuencia de useDeferredValue se ejecutan en background y además que son interrumpibles lo que viene a decirnos que si algo sucede en nuestro componente dentro del estado del mismo antes de que el este re-render que se ejecutará en background termine será interrumpido por parte de React y lo que hace será programar un nuevo re-render en background que tenga en cuenta el nuevo valor.
Llevándolo a nuestro ejemplo el uso de useDeferredValue lanza una proceso de re-render en background (nosotros no lo vemos pero React lo está llevando a cabo) donde se trata de renderizar el componente SlowList sucediendo que antes de que este proceso finalice el atributo del estado query que se ha usado en useDeferredValue cambia de valor lo que hace React es matar el proceso de re-render en background y lanzar uno nuevo (en background) con este nuevo valor repitiéndolo cuantas veces sea necesario como consecuencia de los cambios en el atributo query.
Nota: React nos garantizará siempre que cuando se produce el re-render en background de forma efectiva (se llega a ejecutar) lo que podemos estar seguros es que el original value (en nuestro caso
query) y el deferred value (en nuestro casodeferredQuery) siempre van a tener el mismo valor.
Ahora que hemos visto internamente como funciona useDeferredValue ya podemos entender que en una situación como la que se muestra en la siguiente imagen donde estasmos tecleando caracteres sin parar en nuestro campo de texto el resultado que se corresponde al renderizado de ShowList no se llevará a cabo hasta el momento en el que dejemos de pulsar teclas, es decir, cuando coincidirán query y deferredQuery.

Algunas notas finales
useDeferredValue acepta como parámetro una variable que tiene que ser un tipo de primitivo (`boolean`, string o number). De hecho si tratamos de pasar algo como un objeto al final lo que vamos a tener es un error puesto que cairemos en un bucle infinito. Quier esto decir que si escribimos algo como lo siguiente:
const [query, setQuery] = useState('test')
const deferredValue = useDeferredValue({ query })
Si nos vamos a la consola de JavaScript dentro de nuestro navegador observaremos que estamos en un bucle infinito en el que no se nos parará de mostrar errores:

La razón por la que sucede esto es que si pasamos un objeto o un array como parámetro de useDeferredValue lo que sucederá es que en cada uno de los renders que se lleven a cabo el array o el objeto será diferntes puesto a que pese los elementos del array o los atributos del objeto sean los mismos las referencias que se utilizan para compararlos son diferentes. Por lo tanto, en cada re-render se creará un nuevo objeto o array y su referencia será diferentes a la del objeto o array utilizado en el anterior render y por lo tanto caeremos en un bucle infinito porque nunca se producirá la igual de los mismos.
¿Quiero esto decir que no se podrá pasar un array o un objeto en ninguna situación? La respuesta es que no puesto que sí que vamos a poder pasarlo pero garantizando siempre que el objeto o array ha sido definido fuera de nuestro componente y por lo tanto que siempre va a tener una referencia que será estable o, dicho de otra manera, independiente del proceso de renderizado de nuestros componentes:
const MY_OBJECT = { query: 'test' }
export const Page = () => {
const deferredQuery = useDeferredValue(MY_OBJECT)




