Creando un scroll infinito con React JS! ♾️
Tags: javascript, react, tutorial, spanish
Published: 2022-08-05 13:27:03

En esta ocasión vamos a implementar un scroll infinito usando React JS.  
Una aplicación que implementa scroll infinito consiste en un diseño que permite  a los usuarios seguir consumiendo cierta cantidad de información sin ninguna pausa, ya que el contenido se va cargando automáticamente conforme el usuario vaya haciendo scroll.

> 🚨 Nota: Este post requiere que sepas las bases de **React con TypeScript (hooks básicos)**.
>

Cualquier tipo de Feedback es bienvenido, gracias y espero disfrutes el articulo.🤗


## Tabla de contenido.  
> 📌 [Tecnologías a utilizar.](#2)   
> 📌 [Creando el proyecto.](#3)   
> 📌 [Primeros pasos.](#4)   
> 📌 [Haciendo la petición a la API.](#5)   
> 📌 [Mostrando las tarjetas.](#6)   
> 📌 [Realizando el scroll infinito.](#7)   
> 📌 [Refactorizando.](#8)   
> 📌 [Conclusión.](#9)   
>> 📌 [Demostración en vivo.](#10)   
>> 📌 [Código fuente.](#11)   


&nbsp;


## 🎈 Tecnologías a utilizar. <a id="2"></a>

- ▶️ React JS (version 18)
- ▶️ Vite JS
- ▶️ TypeScript
- ▶️ [React Query](https://tanstack.com/query/v4) 
- ▶️ [Rick and Morty API](https://rickandmortyapi.com/)
- ▶️ CSS vanilla (Los estilos los encuentras en el repositorio al final de este post) 

&nbsp;

## 🎈 Creando el proyecto. <a id="3"></a>

Al proyecto le colocaremos el nombre de: **`infinite-scroll`** (opcional, tu le puedes poner el nombre que gustes).
```bash
npm init vite@latest
```
Creamos el proyecto con Vite JS y seleccionamos React con TypeScript.  
Luego ejecutamos el siguiente comando para navegar al directorio que se acaba de crear.

```bash
cd infinite-scroll
```
Luego instalamos las dependencias.
```bash
npm install
```
Después abrimos el proyecto en un editor de código (en mi caso VS code).
```bash
code .
```

&nbsp;

## 🎈 Primeros pasos. <a id="4"></a>

Primero en el archivo `src/App.tsx` vamos a borrar el contenido y agregamos un titulo.
```tsx
const App = () => {
  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
```
![title](https://res.cloudinary.com/dnxchppfm/image/upload/v1659653948/posts/infinite-scroll/01_nureiv.png)

Luego, vamos a crear dos componentes que vamos a utilizar más tarde. Creamos la carpeta `src/components` y dentro creamos los siguientes archivos:

- **Loading.tsx**   


Este archivo contendrá lo siguiente:
```js
export const Loading = () => {
    return (
        <div className="container-loading">
            <div className="spinner"></div>
            <span>Loading more characters...</span>
        </div>
    )
}
```
Nos servirá para mostrar un spinner cuando se haga una nueva petición a la API. 

- **Card.tsx**


Este archivo contendrá lo siguiente:
```tsx
import { Result } from '../interface';

interface Props {
    character: Result
}
export const Card = ({ character }: Props) => {
    return (
        <div className='card'>
            <img src={character.image} alt={character.name} width={50} loading='lazy' />
            <p>{character.name}</p>
        </div>
    )
}
```
Esta es la tarjeta que mostrara el personaje de Rick and Morty API

En la carpeta `src/interfaces` creamos un archivo index.ts y agregamos las siguientes interfaces.
```ts
export interface ResponseAPI {
    info: Info;
    results: Result[];
}

export interface Info {
    count: number;
    pages: number;
    next: string;
    prev: string;
}

export interface Result {
    id: number;
    name: string;
    image: string;
}
``` 

> 🚨 **Nota**: La interfaz Result en realidad tiene mas propiedades pero en este caso solo usare las que he definido.
>

&nbsp;

## 🎈 Haciendo la petición a la API. <a id="5"></a>

En este caso usaremos la librería **React Query** que nos va a permitir realizar las peticiones de una mejor forma (y que ademas cuenta con otras características como manejo de caché)

- Instalamos la dependencia
```
npm i @tanstack/react-query
```

Y luego en el archivo `src/main.tsx` vamos hacer lo siguiente:

Vamos a encerrar nuestro componente **App** dentro del **QueryClientProvider** y le mandamos el cliente que es solamente una nueva instancia de **QueryClient**. 

```tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'

const queryClient = new QueryClient()

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>
)

```

Ahora en el archivo `src/App.tsx`, vamos a usar un hook especial de **React Query** llamado **useInfiniteQuery**  

```tsx
const App = () => {
     
  useInfiniteQuery()

  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
```

El hook **useInfiniteQuery** necesita varios parámetros:

1 - **queryKey**: un arreglo de strings u objetos anidados, que se usa como clave para gestionar el almacenamiento del cache.

2 - **queryFn**: Una función que devuelve una promesa, la promesa debe estar resuelta o lanzar un error.

3 - **options**: Dentro de las opciones necesitamos una que se llama **getNextPageParam** que es una función que devuelve la información para la siguiente consulta a la API. 

**El primer parámetro es la queryKey** en este caso colocamos un arreglo con la palabra 'characters'

```tsx
const App = () => {
     
  useInfiniteQuery(
        ['characters']
    )

  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
```


**El segundo parámetro es la queryFn** en este caso colocamos un arreglo con la palabra 'characters'


Primero le pasamos una función 
```tsx
const App = () => {
     
  useInfiniteQuery(
        ['characters'],
        () => {}
    )

  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
```

Dicha función debe retornar una promesa resuelta.
Para ello, fuera del componente creamos una función que recibirá como parámetro la pagina a traer y retornara una promesa de tipo **ResponseAPI**.
```tsx
import { ResponseAPI } from "./interface"

const fetcher = (page: number): Promise<ResponseAPI> => fetch(`https://rickandmortyapi.com/api/character/?page=${page}`).then(res => res.json())

const App = () => {
     
  useInfiniteQuery(
        ['characters'],
        () => fetcher()
    )

  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
```

La queryFn recibe diversos parámetros, entre ellos el pageParam que por defecto sera **undefined** y luego numero, asi que si no existe un valor, lo igualaremos a 1. y dicha propiedad se la pasamos a la función **fetcher**.

```tsx
import { ResponseAPI } from "./interface"

const fetcher = (page: number): Promise<ResponseAPI> => fetch(`https://rickandmortyapi.com/api/character/?page=${page}`).then(res => res.json())

const App = () => {
     
  useInfiniteQuery(
        ['characters'],
        ({ pageParam = 1 }) => fetcher(pageParam),
    )

  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
```

Ahora el ultimo parámetro son las options, que es un objeto, el cual usaremos la propiedad **getNextPageParam**


```tsx
import { ResponseAPI } from "./interface"

const fetcher = (page: number): Promise<ResponseAPI> => fetch(`https://rickandmortyapi.com/api/character/?page=${page}`).then(res => res.json())

const App = () => {
     
  useInfiniteQuery(
        ['characters'],
        ({ pageParam = 1 }) => fetcher(pageParam),
        {
            getNextPageParam: () => {}
        }
    )

  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
```
La función **getNextPageParam** recibe dos parámetros pero solo usaremos el primero que es la ultima pagina recibida (o sea la ultima respuesta que nos dio la API).


Dentro de la función, como la API de Rick and Morty no viene ña pagina siguiente (mas bien viene la url para la pagina siguiente) tendremos que realizar lo siguiente: 

**1 - Obtendremos la pagina anterior**

La respuesta de la API viene la propiedad **info** que contiene la propiedad **prev**, evaluamos si existe (porque en la primera llamada la propiedad **prev** es null).
- Si no existe entonces es la pagina 0.
- Si existe entonces obtenemos esa cadena la separamos y obtenemos el número.

```tsx
const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0
```
**2 - Obtendremos la pagina actual**

Solo sumamos la pagina anterior más 1.
```tsx
const currentPage = previousPage + 1;
```

**3 - Evaluaremos si existen más paginas**

Evaluamos si la pagina actual es igual al total de paginas.
- Si es true, entonces retornamos false para que ya no haga otro petición.

- Si es false, entonces retornamos la siguiente pagina, que es el resultado de la suma de la pagina actual mas 1.


```tsx
if ( currentPage === lastPage.info.pages) return false;

return currentPage + 1;
```

Y asi iría quedando el hook.


```tsx
import { ResponseAPI } from "./interface"

const fetcher = (page: number): Promise<ResponseAPI> => fetch(`https://rickandmortyapi.com/api/character/?page=${page}`).then(res => res.json())

const App = () => {
     
  useInfiniteQuery(
        ['characters'],
        
        ({ pageParam = 1 }) => fetcher(pageParam),

        {
            getNextPageParam: (lastPage: ResponseAPI) => {

                const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0
                const currentPage = previousPage + 1;

                if (currentPage === lastPage.info.pages) return false;
                return currentPage + 1;
            }
        }
    )

  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
```

El hook useInfiniteQuery nos da ciertas valores y funciones  de los cuales usaremos los siguientes.

- **data**: un objeto que contiene la consulta de la API
    - Dentro de esta propiedad se encuentra otra llamada **pages** que es un arreglo que contiene las paginas obtenidas, de aquí obtendremos la data de la API.

- **error**: Un mensaje de error causado si la petición a la API falla. 

- **fetchNextPage**: función que permite realizar una nueva petición a la siguiente pagina de la API. 

- **status**: una cadena que contiene los valores "error" | "loading" | "success" indicando el estado de la petición.

- **hasNextPage**: un valor booleano que es verdadero si la función **getNextPageParam** retorna un valor que no sea **undefined** 


```tsx
import { ResponseAPI } from "./interface"

const fetcher = (page: number): Promise<ResponseAPI> => fetch(`https://rickandmortyapi.com/api/character/?page=${page}`).then(res => res.json())

const App = () => {
     
  const { data, error, fetchNextPage, status, hasNextPage } = useInfiniteQuery(
        ['characters'],
        
        ({ pageParam = 1 }) => fetcher(pageParam),

        {
            getNextPageParam: (lastPage: ResponseAPI) => {

                const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0
                const currentPage = previousPage + 1;

                if (currentPage === lastPage.info.pages) return false;
                return currentPage + 1;
            }
        }
    )

  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
```

&nbsp;

## 🎈 Mostrando las tarjetas. <a id="6"></a>

Ahora podemos mostrar los resultados gracias a que ya tenemos acceso a la data.

Creamos un div y dentro vamos a hacer una iteración sobre la propiedad data accediendo a la propiedad page que es un arreglo que por el momento accederemos a la primera posición y a los results.

Ademas evaluamos el status y si es loading mostramos el componente **Loading.tsx** pero si esta en error, colocamos el mensaje de error.

```tsx
import { ResponseAPI } from "./interface"

const fetcher = (page: number): Promise<ResponseAPI> => fetch(`https://rickandmortyapi.com/api/character/?page=${page}`).then(res => res.json())

const App = () => {
     
  const { data, error, fetchNextPage, status, hasNextPage } = useInfiniteQuery(
        ['characters'],
        
        ({ pageParam = 1 }) => fetcher(pageParam),

        {
            getNextPageParam: (lastPage: ResponseAPI) => {

                const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0
                const currentPage = previousPage + 1;

                if (currentPage === lastPage.info.pages) return false;
                return currentPage + 1;
            }
        }
    )

  if (status === 'loading') return <Loading />

  if (status === 'error') return <h4>Ups!, {`${error}` as string}</h4>

  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>

      <div className="grid-container">
        {
          data?.pages[0].results.map(character => (
            <Card key={character.id} character={character} />
          ))
        }
      </div>

    </div>
  )
}
export default App
```
Esto solo muestra la primera pagina, lo siguiente sera implementar el scroll infinito.

![cards](https://res.cloudinary.com/dnxchppfm/image/upload/v1659653949/posts/infinite-scroll/02_wqfdml.png)

&nbsp;

## 🎈 Realizando el scroll infinito. <a id="7"></a>

Para ello vamos a usar una librería popular llamada [react-infinite-scroll-component](https://www.npmjs.com/package/react-infinite-scroll-component).

Instalamos la dependencia.
```
npm i react-infinite-scroll-component
```

Primero necesitamos el componente **InfiniteScroll**.
```tsx
<InfiniteScroll/>
```

Este componente recibirá diversos propiedades
- **dataLength**: La cantidad de elementos, en un momento le colocaremos el valor ya que necesitamos calcularlo. 

- **next**: una función que se disparara cuando se llegue al final de la pagina cuando se haga scroll. Aquí llamaremos a la función que nos ofrece useInfiniteQuery, **fetchNextPage**.

- **hasMore**: propiedad booleana que indica si hay más elementos. Aquí llamaremos a la propiedad que nos ofrece useInfiniteQuery, **hasNextPage**, y lo convertimos en booleano con **!!** porque por defecto es undefined.

- **loader**: componente JSX que se usara para mostrar un mensaje de carga mientras se hace la petición. Aquí llamaremos al componente **Loading.tsx**


```tsx
<InfiniteScroll
    dataLength={}
    next={() => fetchNextPage()}
    hasMore={!!hasNextPage}
    loader={<Loading />}
/>
```

Ahora, la propiedad dataLength podríamos pero esto solo mostraría la pagina siguiente sin acumularse los resultados anteriores, por lo que debemos realizar lo siguiente:

Crearemos una variable memorizada que cambien cada vez que la propiedad **data** de useInfiniteQuery cambie.

Esta variable **characters** debe de retornar un nueva ResponseAPI pero el la propiedad de results se deben acumular los personajes anteriores y los actuales. Y la propiedad info sera la de la pagina actual.

```tsx
const characters = useMemo(() => data?.pages.reduce((prev, page) => {
        return {
            info: page.info,
            results: [...prev.results, ...page.results]
        }
    }), [data])
```

Ahora le pasamos esta constante a **dataLength**, hacemos una evaluación si existe los personajes entonces colocamos la longitud de la propiedad results sino colocamos 0.

```tsx
<InfiniteScroll
    dataLength={characters ? characters.results.length : 0}
    next={() => fetchNextPage()}
    hasMore={!!hasNextPage}
    loader={<Loading />}
/>
```

Ahora dentro del componente debemos colocar la lista a renderizar, de esta manera:

Ahora en vez de iterar sobre **data?.pages[0].results** vamos a iterar sobre la constante memorizada **characters** evaluando si existe.

```tsx
<InfiniteScroll
    dataLength={characters ? characters.results.length : 0}
    next={() => fetchNextPage()}
    hasMore={!!hasNextPage}
    loader={<Loading />}
>
    <div className="grid-container">
        {
            characters && characters.results.map(character => (
                <Card key={character.id} character={character} />
            ))
        }
    </div>
</InfiniteScroll>
```

Y asi quedaría todo completo:

```tsx
import { useMemo } from "react";
import InfiniteScroll from "react-infinite-scroll-component"
import { useInfiniteQuery } from "@tanstack/react-query";

import { Loading } from "./components/Loading"
import { Card } from "./components/Card"

import { ResponseAPI } from "./interface"


const fetcher = (page: number): Promise<ResponseAPI> => fetch(`https://rickandmortyapi.com/api/character/?page=${page}`).then(res => res.json())

const App = () => {
  
  const { data, error, fetchNextPage, status, hasNextPage } = useInfiniteQuery(
        ['characters'],

        ({ pageParam = 1 }) => fetcher(pageParam),

        {
            getNextPageParam: (lastPage: ResponseAPI) => {

                const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0

                const currentPage = previousPage + 1;

                if (currentPage === lastPage.info.pages) return false;
                return currentPage + 1;
            }
        }
    )

    const characters = useMemo(() => data?.pages.reduce((prev, page) => {
        return {
            info: page.info,
            results: [...prev.results, ...page.results]
        }
    }), [data])

  if (status === 'loading') return <Loading />

  if (status === 'error') return <h4>Ups!, {`${error}` as string}</h4>


  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>

      <InfiniteScroll
        dataLength={characters ? characters.results.length : 0}
        next={() => fetchNextPage()}
        hasMore={!!hasNextPage}
        loader={<Loading />}
      >
        <div className="grid-container">
          {
            characters && characters.results.map(character => (
              <Card key={character.id} character={character} />
            ))
          }
        </div>
      </InfiniteScroll>
    </div>
  )
}
export default App
```
Y asi quedaría.

![Final](https://res.cloudinary.com/dnxchppfm/image/upload/v1659655141/posts/infinite-scroll/edit-in-scroll_pi4rdd.gif)

&nbsp;

## 🎈 Refactorizando. <a id="8"></a>
Vamos a crear una nueva carpeta `src/hooks` y agregamos el archivo **useCharacter.ts**
Y movemos toda la lógica.

```ts
import { useMemo } from "react";
import { useInfiniteQuery } from "@tanstack/react-query";
import { ResponseAPI } from "../interface";

export const useCharacter = () => {

    const { data, error, fetchNextPage, status, hasNextPage } = useInfiniteQuery(
        ['characters'],
        ({ pageParam = 1 }) => fetch(`https://rickandmortyapi.com/api/character/?page=${pageParam}`).then(res => res.json()),
        {
            getNextPageParam: (lastPage: ResponseAPI) => {

                const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0
                const currentPage = previousPage + 1;

                if (currentPage === lastPage.info.pages) return false;
                return currentPage + 1;
            }
        }
    )

    const characters = useMemo(() => data?.pages.reduce((prev, page) => {
        return {
            info: page.info,
            results: [...prev.results, ...page.results]
        }
    }), [data])

    return {
        error, fetchNextPage, status, hasNextPage,
        characters
    }
}
```
**Ahora en `src/App.tsx` es más fácil de leer.**

```ts
import InfiniteScroll from "react-infinite-scroll-component"

import { Loading } from "./components/Loading"
import { Card } from "./components/Card"

import { useCharacter } from './hooks/useCharacter';

const App = () => {
  const { characters, error, fetchNextPage, hasNextPage, status } = useCharacter()

  if (status === 'loading') return <Loading />

  if (status === 'error') return <h4>Ups!, {`${error}` as string}</h4>


  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>

      <InfiniteScroll
        dataLength={characters ? characters.results.length : 0}
        next={() => fetchNextPage()}
        hasMore={!!hasNextPage}
        loader={<Loading />}
      >
        <div className="grid-container">
          {
            characters && characters.results.map(character => (
              <Card key={character.id} character={character} />
            ))
          }
        </div>
      </InfiniteScroll>
    </div>
  )
}
export default App
```

&nbsp;

## 🎈 Conclusión. <a id="9"></a>

Todo el proceso que acabo de mostrar, es una de las formas en que se puede implementar scroll infinito de una manera rápida usando paquetes de terceros. ♾️

Espero haberte ayudado a entender como realizar este diseño,muchas gracias por llegar hasta aquí! 🤗❤️

Te invito a que comentes si es que este articulo te resulta útil o interesante, o si es que conoces alguna otra forma distinta o mejor de como implementar un scroll infinito. 🙌

&nbsp;

### 🎈 Demostración en vivo. <a id="10"></a>

[https://infinite-scroll-app-fml.netlify.app](https://infinite-scroll-app-fml.netlify.app/)

&nbsp;

### 🎈 Código fuente. <a id="11"></a>
{%embed https://github.com/Franklin361/infinite-scroll %}