DEV Community

Cover image for Renderizado de 10.000 filas en React
DevJoseManuel
DevJoseManuel

Posted on

Renderizado de 10.000 filas en React

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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}`
  }))
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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} />
Enter fullscreen mode Exit fullscreen mode

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} />} />
Enter fullscreen mode Exit fullscreen mode

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 props key 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 }}
/>
Enter fullscreen mode Exit fullscreen mode

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} />}
/>
Enter fullscreen mode Exit fullscreen mode

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 prop className. 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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}
/>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

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}
/>
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

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 prop itemContent 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}
/>
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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])
}
Enter fullscreen mode Exit fullscreen mode

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}
/>
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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])
}
Enter fullscreen mode Exit fullscreen mode

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}
/>
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)