En este artículo vamos a ver la forma en la que vamos a poder renderizar 10.000 filas de elementos de forma eficiente y para ello la cantidad de código que vamos a utilizar es más bien poca pero el grado de rendimiento (performance) que lograremos será muy alto.
Código de partida
Para entender bien el problema al que nos estamos enfrentando vamos a suponer que estamos ante el siguiente componente de React el cual tiene declara un atributo de su estado denominado users
gracias al uso del hook useState
:
import { useState } from 'react'
import { UserCard } from './components/UserCard'
import { createUsers } from './user'
export const App = () => {
const [users, setUsers] = useState(createUsers)
return (
<div>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
)
}
En el código de nuestro componente podemos ver cómo el valor del atributo del estado users
está siendo inicializado con el resultado de la invocación de la función createUsers
que no es más que una función auxiliar dentro de nuestro código que se utilizará para crear un array de 10.000 usuarios sabiendo que un usuario User
no es más que un objeto que tiene los atributos id
y name
:
export type User = {
id: number
name: string
}
export const createUsers = (from = 0, to = 10_000): User[] => {
return Array.from({ length: to - from }, (_, i) => ({
id: i + from,
name: `User ${i + from}`
}))
}
Siguiendo con el código del componente App
y en lo que respecta a la parte del JSX lo que estamos haciendo iterar sobre todos los elementos que están recogidos en el atributo del estado users
e iteramos sobre los mismos pasándoselos en la prop user
del componente UserCard
el cual es un componente muy sencillo en el que simplemente estaremos mostrando el nombre del usuario que se está recibiendo:
import { User } from './User'
export const UserCard = ({ user }: User) => {
return <div>{user.name}</div>
}
Problema
Como podemos intuir el problema al que nos estamos enfrentando desde el punto de vista del rendimiento es que este será realmente pésimo si tomamos los 10.000 usuarios y los escribimos todos directamente gracias a la utilización de la función map
como en el código que se ha presentado.
Solución
Para lograr solucionar el problema de rendimiento que hay detrás de situaciones como las que acabamos de describir una solución pasa por utilizar una librería de virtualización que nos ayude en nuestro trabajo. Pero ¿qué es eso de la virtualización? Pues se trata de un concepto realmente sencillo que lo que viene a decir es que únicamente se ha de renderizar un subconjunto de elementos en lugar de todos ellos en función de determinadas condiciones.
¿Y de qué tipo pueden ser estas condiciones? Pues en el caso de nuestro ejemplo lo que nos gustaría es que únicamente se renderizasen en el DOM aquellos usuarios que son visibles en el viewport (que en nuestro caso será la pantalla de un ordenador pero podría ser la pantalla de un dispositivo móvil o de una tablet).
De entre todas las opciones que tenemos a nuestra disposición nosotros nos vamos a decantar por el uso de la librería Virtuoso puesto que se trata de una de las librerías más populares para poder lograr el objetivo que estamos persiguiendo (la virtualización de los usuarios) además de ser un proyecto que está muy bien mantenido.
Nota: se puede obtener más información sobre Virtuoso en su sitio web.
Uso de Virtuoso
Como sucede con cualquier otra librería con la que podemos trabajar en nuestros proyectos lo primero que vamos a tener que hacer será instalarla para lo cual abriremos una terminal del sistema dentro de nuestro proyecto y ejecutaremos:
$ yarn add react-virtuoso
Una vez tenemos la librería instalada vamos a comenzar a hacer los cambios en el código que nos permitan trabajar con la librería y el primer cambio que vamos a realizar consistirá en eliminar el código que ejecta la función map()
dentro del JSX para poder renderizar los usuarios puesto que nuestro objetivo será que Virtuoso sea el que acabe renderizando los usuarios:
export const App = () => {
const [users, setUsers] = useState(createUsers)
return <div></div>
}
Ahora deberemos importar Virtuoso para poder trabajar con la librería por lo que añadimos la siguiente línea de código:
import { Virtuoso } from 'react-virtuoso'
Nota: al haber instalado Virtuoso en nuestro proyecto vamos a tener muchos más componentes a nuestra disposición a partir de
Virtuoso
los cuáles iremos viendo a medida que vayamos avanzando en el artículo.
Una vez tenemos Virtuoso
importado en nuestro componente su uso es muy sencillo puesto que simplemente deberemos escribir algo como lo siguiente:
export const App = () => {
const [users, setUsers] = useState(createUsers)
return (
<div>
<Virtuoso />
</div>
)
}
Evidentemente con el código anterior no habremos logrado nada puesto que Virtuoso
va a necesitar utilizar tres props para lograr su objetivo. En primer lugar tenemos la prop data
que es donde deberemos especificar los datos de partida de los que queremos que Virtuoso haga las tareas de virtualización. En nuestro caso estos datos serán los usuarios con los que estamos trabajando o, dicho de otra manera, el atributo del estado users
.
<Virtuoso data={users} />
La segundo prop que tenemos que considerar esitemContent
la cual es una función que será la que utilizaremos para que se pueda renderizar cada uno de los elementos que están virtualizados de forma individual. En nuestro caso lo que vamos a tener que hacer es proporcionarle una función que se encargue del renderizado de cada uno de los usuarios de forma individual. Si estamos usando un editor como VS Code para escribir nuestro código podemos ver que la función que debemos utilizar tiene que tener definida dos argumentos siendo el primero index
(que es un número) y el segundo argumento será data
que será del tipo de datos que está asociado a cada uno de los elementos que forman parte del array proporcionado en la prop data
(que en nuestro caso serán del tipo User
):
Como en nuestro caso no vamos a hacer uso del índice lo que haremos en la función que vamos a asignar a itemContent
es establecer un caracter de subrayado _
como el primero de los parámetros puesto que esta es una convención de código que se sigue en el caso de que se especifique un argumento de una función pero que realmente no usaremos. Es más en el cuerpo de la función lo que haremos será llamar al componente UserCard
pasándole el usuario que se ha recibido como segundo parámetro:
<Virtuoso data={users} itemContent={(_, user) => <UserCard user={user} />} />
Nota: es importante resaltar que cuando estamos usando la función
itemContent
para renderizar el contenido asociado a cada uno de los elementos que se están renderizando no es necesario hacer uso de la propskey
como sucede cuando estamos renderizando elementos con dentro de una iteración en JSX y la razón por la que esto no es necesario es porque Virtuoso estará proporcionándola por nosotros sin que nos demos cuenta.
Nos queda una tercera prop en la que le proporcionaremos a Virtuoso
cuál es la altura actual (height) del componente puesto que es una pieza de información importante para poder determinar qué elementos serán visibles y cuáles no. Para proporcionar esta información podemos usar la prop style
a la que le podemos asignar un objeto que define los estilos que queremos que se apliquen al componente Virtuoso
. En nuestro caso vamos a suponer que el componente va a tener una altura de 200 píxeles por lo que escribiremos:
<Virtuoso
data={users}
itemContent={(_, user) => <UserCard user={user} />}
style={{ height: 200 }}
/>
Ahora bien, suponiendo que en nuestro proyecto estamos usando Tailwind ¿cómo podemos especificar esta altura? Pues en estas situaciones tenemos que hacer uso de la prop className
en al que especificaremos el tamaño que queremos para el componente con una exclamación !
delante para indicar de esta manera que el tamaño especificado es !important
y por lo tanto que es el que ha de ser considerado:
<Virtuoso
className="!h-[200px]"
data={users}
itemContent={(_, user) => <UserCard user={user} />}
/>
Nota:: la razón por la que hemos tenido que usar la exclamación para poder especificar el tamaño del componente
Virtuoso
es porque en el caso de que no especifiquemos nosotros una altura esta será determinada de forma automática por el Virtuoso y esa altura que se determina automáticamente tendrá más preferencia que la que se le asigne en la propclassName
. Sin embargo al hacer uso de la!
estaremos indicando que el valor especificado es!important
desde el punto de vista de CSS y por lo tanto será el que se tendrá en cuenta como altura del componente.
El código completo del componente App
que recoge todos los cambios que se han realizado para utilizar Virtuoso son los que se pueden ver a continuación:
import { useState } from 'react'
import { Virtuoso } from 'react-virtuoso'
import { UserCard } from './components/UserCard'
import { createUsers } from './user'
export const App = () => {
const [users, setUsers] = useState(createUsers)
return (
<div>
<Virtuoso
className="!h-[200px]"
data={users}
itemContent={(_, user) => <UserCard user={user} />}
/>
</div>
)
}
Si ahora guardamos nuestro trabajo y nos dirigimos a nuestro navegador para ver cómo funciona nuestra aplicación nos encontraremos con algo parecido a lo que se puede ver en la siguiente imagen donde podemos ver que se hace scroll en la página y no tenemos ningún tipo de problema de rendimiento lo cual es justamente lo que queríamos:
Navegar de forma aleatoria a un elemento
Para poder seguir profundizando en el uso de Virtuoso vamos a crear un botón dentro de nuestra interfaz de usuario que al ser pulsado lo que haga sea navegar de forma aleatoria (haciendo scroll) a uno de los elementos del array de usuarios con el que estamos trabajando con la intención de ver cómo se comportará el rendimiento de nuestra aplicación en un caso como este.
Para lograr nuestro objetivo lo primero que vamos a hacer es crear una ref
de React puesto que posteriormente se la vamos a proporcionar a Virtuoso para poder trabajar con ella dentro de la función que se encargará de navegar a un elemento concreto dentro del array de usuarios. Así comenzaremos escribiendo el siguiente código en el que importaremos todos los elementos adicionales que necesitaremos:
import { userRef, useState } from 'react'
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'
Y ahora lo que hacemos será declarar la ref
con la que vamos a trabajar como sigue:
export const App = () => {
const [users, setUsers] = useState(createUsers)
const virtuosoRef = useRef<VirtuosoHandle>(null)
Una vez que tenemos la ref
declarada en el código lo siguiente que vamos a tener que hacer es utilizarla dentro del código JSX para asignársela al componente Virtuoso
como sigue:
<Virtuoso
className="!h-[200px]"
data={users}
itemContent={(_, user) => <UserCard user={user} />}
ref={virtuosoRef}
/>
Hecho esto el siguiente paso que vamos a tener que dar consistirá en la creación de un botón normal del HTML (es decir, en la creación de un nuevo elemento <button>
) donde en su atributo onClick
lo que vamos a hacer es, a través de la virtuosoRef
llamar al método scrollToIndex()
que nos proporcionará el cual espera recibir como parámetro un objeto donde en su atributo index
lo que haremos será pasarle un valor aleatorio que represente el índice del array de usuarios al que queremos desplazarnos:
<button
onClick={() => {
virtuosoRef.current?.scrollToIndex({
index: Math.random() * users.length
})
}}
>
Scroll
</button>
Nota: simplemente recordar que
Math.random()
lo que hace es retornar un valor entre 0 y 1 que al multiplicarlo por el número de usuarios que tenemos lo que hará será proporcionarnos un número comprendido entre 0 y 9999.
Adicionalmente en el objeto con las opciones que se le pasan a scrollToIndex
vamos a pasarle el atributo align
en el que tenemos que especificar cómo queremos que se alinee el elemento al que se va a realizar el scroll con respecto al resto de los que se muestran dentro del Virtuoso siendo las posibilidades center
(en el centro del los elementos que se renderizan), start
(al inicio de las lista de elementos) y end
al final. En nuestro caso vamos a decantarnos por la opción start
:
<button
onClick={() => {
virtuosoRef.current?.scrollToIndex({
align: 'center',
index: Math.random() * users.length
})
}}
>
Scroll
</button>
Por último vamos a añadir una clase de Tailwind para el botón deje un espacio (margen) con respecto a la lista de elementos:
<button
className="mb-4"
onClick={() => {
virtuosoRef.current?.scrollToIndex({
align: 'center',
index: Math.random() * users.length
})
}}
>
Scroll
</button>
Si ahora guardamos nuestra aplicación y volvemos al navegador podemos ver que inicialmente se estarán mostrando los usuarios que inicialmente van entran dentro del espacio (altura) que le hemos asignado a Virtuoso pero cuando pulsamos sobre el botón Scroll podemos ver cómo se realiza el desplazamiento a uno de ellos de forma aleatoria quedando el elemento seleccionado (el elemento al que se va a desplazar) en el centro:
Como podemos ver el desplazamiento que se produce en la lista de usuarios se produce de forma instantánea lo cual totalmente deseable y mejora el rendimiento de nuestra aplicación. Podemos pulsar tantas veces como queramos en el botón y el desplazamiento se producirá de forma instantánea.
Otra de las opciones que tenemos a la hora de definir cómo queremos que se lleve realice el scroll al elemento que se ha seleccionado de forma aleatoria es gracias a la definición del atributo behavior
dentro del objeto de propiedades que recibe el método scrollToIndex()
que podrá adoptar los valores auto
(que es el valor por defecto) o smooth
(que hará que el desplazamiento sea mucho más suave):
<button
className="mb-4"
onClick={() => {
virtuosoRef.current?.scrollToIndex({
align: 'center',
behavior: 'smooth',
index: Math.random() * users.length
})
}}
>
Scroll
</button>
Y si ahora guardamos nuestro trabajo y volvemos al navegador podemos observar como al pulsar sobre el botón Scroll el desplazamiento será mucho más suave:
Nota: tenemos que entender que internamente Virtuoso lo que está haciendo es tener en cuenta el tamaño que tiene que asignado (altura), determinar cuántos elementos podrán ser renderizados dentro de el y únicamente renderizará aquellos que dentro del índice que se está mostrando quepan dentro del dicho tamaño.
El código del componente completo tras incorporar todas las modificaciones que acabamos de explicar es el que se puede ver a continuación:
import { useState } from 'react'
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'
import { UserCard } from './components/UserCard'
import { createUsers } from './user'
export const App = () => {
const [users, setUsers] = useState(createUsers)
const virtuosoRef = useRef<VirtuosoHandle>(null)
return (
<div>
<button
className="mb-4"
onClick={() => {
virtuosoRef.current?.scrollToIndex({
align: 'center',
behavior: 'smooth',
index: Math.random() * users.length
})
}}
>
Scroll
</button>
<Virtuoso
className="!h-[200px]"
data={users}
itemContent={(_, user) => <UserCard user={user} />}
ref={virtuosoRef}
/>
</div>
)
}
Uso de Virtuoso con una tabla.
Vamos a ir un paso más adelante viendo como podemos usar Virtuoso cuando la lista de los usuarios que vamos a renderizar está dentro de una tabla puesto que este es uno de los casos de uso más habituales que nos podemos encontrar cuando tenemos que trabajar con grandes cantidades de datos.
Lo primero que vamos a tener que hacer es cambiar el componente Virtuoso
por el componente TableVirtuoso
por lo que lo que vamos hacer es definir el import
que necesitaremos:
import { TableVirtuoso, VirtuosoHandle } from `react-virtuoso`
E inicialmente el único cambio que vamos a tener que hacer en el código JSX con el que estamos trabajando hasta ahora será reemplazar el la llamada al componente Virtuoso
por la llamada al componente TableVirtuoso
como se puede ver a continuación:
<TableVirtuoso
className="!h-[200px]"
data={users}
itemContent={(_, user) => <UserCard user={user} />}
ref={virtuosoRef}
/>
De todas maneras para que TableVirtuoso
funcione correctamente vamos a necesitar pasarle alguna prop más. Ahora bien, teniendo en cuenta que TableVirtuoso
lo que espera es que en la función que está asociada a itemContent
deberá renderizar elementos que forman parte de una tabla. Esto quiere decir que, en nuestro caso, como estamos haciendo uso del componente UserCard
para renderizar cada uno de los elementos de la lista con la que estamos trabajando lo que vamos a hacer es modificar el código para que renderice filas de la tabla:
import { User } from './user'
export const UserCard = ({ user }: { user: User }) => {
return (
<>
<td>{user.id}</td>
<td>{user.name}</td>
</>
)
}
Nota: en el caso de estar trabajando con un
TableVirtuoso
lo que tenemos que pensar es que el código JSX que ha de retornar la función definida en su propitemContent
será encerrado entre nodos<tr>
de HTML (es decir, que formarán una fila de la tabla) razón por la que estaremos devolviendo elementos<td>
en el codigo anterior.
Es más, como estamos trabajando con una tabla deberíamos tener una manera de poder especificar cuáles los encabezados de esta tabla y para ello TableVirtuoso
nos ofrece la prop fixedHeaderContent
la cual espera recibir como valor una función que ha de retornar un ReactNode
(código JSX) pero en este caso tendremos que especificar que queremos que se cree una nueva fila en la tabla (la primera que quedará fija) y el encabezado que representa el contenido de las columnas, por lo que escribiremos algo como lo siguiente:
<TableVirtuoso
className="!h-[200px]"
data={users}
fixedHeaderContent={() => ({
<tr>
<th className='w-[150px] bg-grayscale-700 text-left'>Id</th>
<th className='w-[150px] bg-grayscale-700 text-left'>Name</th>
</tr>
})}
itemContent={(_, user) => <UserCard user={user} />}
ref={virtuosoRef}
/>
Si ahora guardamos nuestro trabajo y nos vamos al navegador nos encontraremos algo como lo que se puede ver en la siguiente imagen donde vemos que podemos hacer scroll sobre los elementos de la tabla dejando fijo el encabezado que hemos definido tal y como esperábamos:
Y no solamente eso sino que además si pulsamos sobre el botón Scroll el funcionamiento del scroll seguirá siendo el mismo que hemos visto anteriormente:
El código completo cuando estamos trabajando con tablas de elementos con Virtuoso que recogerá los cambios que hemos estado describiendo hasta este punto será el que se puede ver a continuación:
import { useState } from 'react'
import { TableVirtuoso, VirtuosoHandle } from `react-virtuoso`
import { UserCard } from './components/UserCard'
import { createUsers } from './user'
export const App = () => {
const [users, setUsers] = useState(createUsers)
const virtuosoRef = useRef<VirtuosoHandle>(null)
return (
<div>
<button
className="mb-4"
onClick={() => {
virtuosoRef.current?.scrollToIndex({
align: 'center',
behavior: 'smooth',
index: Math.random() * users.length
})
}}
>
Scroll
</button>
<TableVirtuoso
className="!h-[200px]"
data={users}
fixedHeaderContent={() => ({
<tr>
<th className='w-[150px] bg-grayscale-700 text-left'>Id</th>
<th className='w-[150px] bg-grayscale-700 text-left'>Name</th>
</tr>
})}
itemContent={(_, user) => <UserCard user={user} />}
ref={virtuosoRef}
/>
</div>
)
}
Nota: en lo que respecta a las posibilidades de personalización de la tabla son muchas teniendo en cuenta que podemos personalizar tantos los elementos del encabezado como cada uno de los elementos que forman las filas de la tabla.
Datos paginados
Hasta ahora hemos visto la forma en la que gracias a Virtuoso vamos a poder renderizar una gran cantidad de elementos de forma eficiente en nuestras aplicaciones de React pero lo más probable es que en un proyecto real en el que estemos trabajando lo habitual sea que no tengamos todos los elementos a mostrar de una única vez sino que los vayamos obteniendo de forma paginada.
Para ver cómo podemos solucionar este tipo de situaciones vamos a comenzar haciendo unos pequeños cambios en el código que hemos desarrollado hasta ahora comenzando por la función createUsers
donde en vez de crear los 10_000 usuarios de una única vez y retornarlo vamos a crear algo más razonable como podrían ser 20 usuarios. Así, cuando dentro del componente App
llamemos a createUsers
para obtener el número de usuarios con el que vamos a trabajar simplemente invocaremos a createUsers
pasándole como parámetros 0 y 20 para indicar de esta manera que queremos obtener los 20 primeros usuarios.
export const App = () => {
const [users, setUsers] = useState(() => createUsers(0, 20))
El siguiente paso que vamos a dar será crear la función que nos permita obtener la siguiente página de datos, es decir, los siguientes 20 usuarios que se han de mostrar en la página. Así pues escribiremos:
const fetchNextPage = () => {
const newUsers = createUsers(users.length, users.length + 20)
setUsers([...users, ...newUsers])
}
Como podemos observar lo que estamos haciendo es volver a llamar a la función createUser
pero en este caso la petición de usuarios comenzará por el número de usuarios que tenemos hasta este momento (tras la primera llamada serán los 20 primeros usuarios) y como segundo el tamaño de la página (el número de usuarios que se han de mostrar) es el que dicta que queremos los siguientes 20 usuarios, es decir, que tras la primera llamada querremos los usuarios que van desde el 20 hasta el 40.
Nota: evidentemente en una aplicación más compleja la forma de obtener la siguiente página de datos sería seguramente más compleja que lo que acabamos de mostrar pero a efectos de lo que estamos explicando esta manera nos ayuda a aclarar lo que queremos conseguir.
Por último en la función fetchNexPage
simplemente lo que hacemos es actualizar el atributo del estado users
con un nuevo array que tenga a los usuarios que había hasta ese momento más los nuevos que hemos obtenido.
La idea ahora es poder conectar a Virtuoso con la función fetchNextPage
cuando se alcance el final de los datos que tiene en ese momento disponible para poder ser renderizados y de esta manera que sea el mismo quien se encargue de hacer la petición de la siguiente página. Para ello el componente TableVirtuoso
(o el componente Virtuoso
en el caso de que no estemos trabajando con datos dentro de una tabla) nos ofrece la prop endReached
a la que se le debe asignar la función que queremos que sea ejecutada cuando se alcanza el final de los datos que se están mostrando. Por lo tanto escribiremos:
<TableVirtuoso
className="!h-[200px]"
data={users}
endReached={fetchNextPage}
fixedHeaderContent={() => ({
<tr>
<th className='w-[150px] bg-grayscale-700 text-left'>Id</th>
<th className='w-[150px] bg-grayscale-700 text-left'>Name</th>
</tr>
})}
itemContent={(_, user) => <UserCard user={user} />}
ref={virtuosoRef}
/>
Si ahora guardamos nuestros cambios y nos vamos al navegador vemos que el scroll es fluido y que se van obteniendo los datos tal y como esperábamos:
El código completo del componente App
una vez incorporamos todas las modificaciones que hemos ido realizando hasta ahora es el que se puede ver a continuación:
import { useState } from 'react'
import { TableVirtuoso, VirtuosoHandle } from `react-virtuoso`
import { UserCard } from './components/UserCard'
import { createUsers } from './user'
export const App = () => {
const [users, setUsers] = useState(() => createUsers(0, 20))
const virtuosoRef = useRef<VirtuosoHandle>(null)
const fetchNextPage = () => {
const newUsers = createUsers(users.length, users.length + 20)
setUsers([...users, ...newUsers])
}
return (
<div>
<button
className="mb-4"
onClick={() => {
virtuosoRef.current?.scrollToIndex({
align: 'center',
behavior: 'smooth',
index: Math.random() * users.length
})
}}
>
Scroll
</button>
<TableVirtuoso
className="!h-[200px]"
data={users}
endReached={fetchNextPage}
fixedHeaderContent={() => ({
<tr>
<th className='w-[150px] bg-grayscale-700 text-left'>Id</th>
<th className='w-[150px] bg-grayscale-700 text-left'>Name</th>
</tr>
})}
itemContent={(_, user) => <UserCard user={user} />}
ref={virtuosoRef}
/>
</div>
)
}
Datos asíncronos
Es más que probable que los datos a partir de los cuáles esté trabajando Virtuoso provengan de una fuente asíncrona como podría ser una llamada a una API así que vamos a detenernos unos instantes en ver cómo los podemos incorporar a nuestra implementación.
Para ello lo primero que vamos a hacer es crear un nuevo atributo del estado que se encargue de definir cuándo se están cargando los datos:
export const App = () => {
const [users, setUsers] = useState(() => createUsers(0, 20))
const [isLoading, setIsLoading] = useState(false)
Para simular la llamada asíncrona lo que vamos a hacer es convertir la función fetchNextPage
en una función asíncrona y dentro de la misma vamos a hacer una llamada a una Promesa de JavaScript la cual será resuelta una vez pasa 1000 milisegundos (es decir, 1 segundo) de tal manera que lo que estaremos simulando es que la llamada para la obtención de los datos de la API será asíncrona:
const fetchNextPage = async () => {
const newUsers = createUsers(users.length, users.length + 20)
setIsLoading(true)
await new Promise(resolve => setTimeout(resolve, 1_000))
setIsLoading(false)
setUsers([...users, ...newUsers])
}
En el código anterior podemos ver como además antes de que se llame a la Promisa de JavaScript estaremos estableciendo el valor del atributo del estado isLoading
como true
indicando de esta manera que se están cargando los datos y una vez se resuelve la Promesa establecemos el valor de este atributo a false
indicando así que la carga de los datos ha finalizado.
Como podemos imaginar gracias al atributo del estado isLoading
vamos a poder mostrarle al usuario un indicador que nos ayude a renderizar algo dentro de la interfaz de usuario que le indicará que los datos se están obteniendo. En el caso de TableVirtuoso
tenemos a nuestra disposición la prop fixedFooterContent
la cual espera tener asignada una función que se ejecutará cada vez que se re-renderice TableVirtuoso
con el fin de mostrar el contenido del pie de la página.
¿Y qué es lo que queremos hacer nosotros? Pues simplemente que en el caso de que los datos se esté cargando (es decir, que el valor del atributo del estado isLoading
sea true
) mostraremos un mensaje de cargando información o en el caso de que no sea así simplemente retornaremos undefined
indicando de esta manera que no se tiene que ejecutar ninguna función y por lo tanto no se escribirá nada en el pie de la tabla.
<TableVirtuoso
className="!h-[200px]"
data={users}
endReached={fetchNextPage}
fixedFooterContent={ isLoading
? () => <div className='bg-grayscale-700'>Loading...</div>
: undefined
}
fixedHeaderContent={() => ({
<tr>
<th className='w-[150px] bg-grayscale-700 text-left'>Id</th>
<th className='w-[150px] bg-grayscale-700 text-left'>Name</th>
</tr>
})}
itemContent={(_, user) => <UserCard user={user} />}
ref={virtuosoRef}
/>
Si ahora guardamos los cambios y volvemos al navegador podemos ver que en el momento en el que hacemos scroll y se alcanza el número de usuarios que tenemos paginados se muestra en la parte inferior de la tabla el mensaje loading indicando de esta manera que se están pidiendo nuevos datos con los que completar la información de la tabla tal y como esperábamos:
El código completo del componente App
una vez incorpora la posibilidad de la obtención de los datos de forma asíncrona es el siguiente:
import { useState } from 'react'
import { TableVirtuoso, VirtuosoHandle } from `react-virtuoso`
import { UserCard } from './components/UserCard'
import { createUsers } from './user'
export const App = () => {
const [users, setUsers] = useState(() => createUsers(0, 20))
const [isLoading, setIsLoading] = useState(false)
const virtuosoRef = useRef<VirtuosoHandle>(null)
const fetchNextPage = async () => {
const newUsers = createUsers(users.length, users.length + 20)
setIsLoading(true)
await new Promise(resolve => setTimeout(resolve, 1_000))
setIsLoading(false)
setUsers([...users, ...newUsers])
}
return (
<div>
<button
className="mb-4"
onClick={() => {
virtuosoRef.current?.scrollToIndex({
align: 'center',
behavior: 'smooth',
index: Math.random() * users.length
})
}}
>
Scroll
</button>
<TableVirtuoso
className="!h-[200px]"
data={users}
endReached={fetchNextPage}
fixedFooterContent={ isLoading
? () => <div className='bg-grayscale-700'>Loading...</div>
: undefined
}
fixedHeaderContent={() => ({
<tr>
<th className='w-[150px] bg-grayscale-700 text-left'>Id</th>
<th className='w-[150px] bg-grayscale-700 text-left'>Name</th>
</tr>
})}
itemContent={(_, user) => <UserCard user={user} />}
ref={virtuosoRef}
/>
</div>
)
}
Top comments (0)