Renderizado de 10.000 filas en React
Tags: react, spanish
Published: 2024-04-28 07:43:43

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`:

```tsx
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`:

```ts
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:

```tsx
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](https://virtuoso.dev/).

## 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:

```bash
$ 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:

```tsx
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:

```tsx
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:

```tsx
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`.

```tsx
<Virtuoso data={users} />
```

La segundo prop que tenemos que considerar es`itemContent` 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`):

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o9rogqdoqnrhse69v7rn.png)

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:

```tsx
<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 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:

```tsx
<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:

```tsx
<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 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:

```tsx
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:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dmyksqc7iru6ubmgnu6f.gif)

## 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:

```tsx
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:

```tsx
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:

```tsx
<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:

```tsx
<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`:

```tsx
<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:

```tsx
<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:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u9i9zqtj8rvffnc77xfe.gif)

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):

```tsx
<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:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ks52rpvzb1bm95usc5bx.gif)

> **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:

```tsx
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:

```tsx
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:

```tsx
<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:

```tsx
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 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:

```tsx
<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:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l9897wt6dc1og5f4cu87.gif)

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:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/abzuex8bkikkhylvzh1r.gif)

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:

```tsx
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.

```tsx
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:

```tsx
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:

```tsx
<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:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6x71y2mrmztheewiceqq.gif)

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:

```tsx
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:

```tsx
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:

```tsx
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.

```tsx
<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:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3o10m0nujboq58z7c025.gif)

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:

```tsx
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>
  )
}
```