DEV Community 👩‍💻👨‍💻

Cover image for Creando un scroll infinito con React JS! ♾️
Franklin Martinez
Franklin Martinez

Posted on

Creando un scroll infinito con React JS! ♾️

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.

📌 Creando el proyecto.

📌 Primeros pasos.

📌 Haciendo la petición a la API.

📌 Mostrando las tarjetas.

📌 Realizando el scroll infinito.

📌 Refactorizando.

📌 Conclusión.

📌 Demostración en vivo.

📌 Código fuente.

 

🎈 Tecnologías a utilizar.

  • ▶️ React JS (version 18)
  • ▶️ Vite JS
  • ▶️ TypeScript
  • ▶️ React Query
  • ▶️ Rick and Morty API
  • ▶️ CSS vanilla (Los estilos los encuentras en el repositorio al final de este post)

 

🎈 Creando el proyecto.

Al proyecto le colocaremos el nombre de: infinite-scroll (opcional, tu le puedes poner el nombre que gustes).

npm init vite@latest
Enter fullscreen mode Exit fullscreen mode

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.

cd infinite-scroll
Enter fullscreen mode Exit fullscreen mode

Luego instalamos las dependencias.

npm install
Enter fullscreen mode Exit fullscreen mode

Después abrimos el proyecto en un editor de código (en mi caso VS code).

code .
Enter fullscreen mode Exit fullscreen mode

 

🎈 Primeros pasos.

Primero en el archivo src/App.tsx vamos a borrar el contenido y agregamos un titulo.

const App = () => {
  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

title

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:

export const Loading = () => {
    return (
        <div className="container-loading">
            <div className="spinner"></div>
            <span>Loading more characters...</span>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Nos servirá para mostrar un spinner cuando se haga una nueva petición a la API.

  • Card.tsx

Este archivo contendrá lo siguiente:

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

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.

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

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

 

🎈 Haciendo la petición a la API.

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

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.

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

Enter fullscreen mode Exit fullscreen mode

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

const App = () => {

  useInfiniteQuery()

  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

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'

const App = () => {

  useInfiniteQuery(
        ['characters']
    )

  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

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

Primero le pasamos una función

const App = () => {

  useInfiniteQuery(
        ['characters'],
        () => {}
    )

  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

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

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

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

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.
const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0
Enter fullscreen mode Exit fullscreen mode

2 - Obtendremos la pagina actual

Solo sumamos la pagina anterior más 1.

const currentPage = previousPage + 1;
Enter fullscreen mode Exit fullscreen mode

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.

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

return currentPage + 1;
Enter fullscreen mode Exit fullscreen mode

Y asi iría quedando el hook.

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

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

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

 

🎈 Mostrando las tarjetas.

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.

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

Esto solo muestra la primera pagina, lo siguiente sera implementar el scroll infinito.

cards

 

🎈 Realizando el scroll infinito.

Para ello vamos a usar una librería popular llamada react-infinite-scroll-component.

Instalamos la dependencia.

npm i react-infinite-scroll-component
Enter fullscreen mode Exit fullscreen mode

Primero necesitamos el componente InfiniteScroll.

<InfiniteScroll/>
Enter fullscreen mode Exit fullscreen mode

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

<InfiniteScroll
    dataLength={}
    next={() => fetchNextPage()}
    hasMore={!!hasNextPage}
    loader={<Loading />}
/>
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

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

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.

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

Y asi quedaría todo completo:

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

Y asi quedaría.

Final

 

🎈 Refactorizando.

Vamos a crear una nueva carpeta src/hooks y agregamos el archivo useCharacter.ts
Y movemos toda la lógica.

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

Ahora en src/App.tsx es más fácil de leer.

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

 

🎈 Conclusión.

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. 🙌

 

🎈 Demostración en vivo.

https://infinite-scroll-app-fml.netlify.app

 

🎈 Código fuente.

GitHub logo Franklin361 / infinite-scroll

Creating an infinite scroll with react js ♾️

Creating an infinite scroll with React JS! ♾️

This time, we are going to implement the infinite scroll layout using React JS and other libraries!

 

Demo

 

Features ⚙️

  1. View cards.
  2. Load more cards while scrolling.

 

Technologies 🧪

  • ▶️ React JS (version 18)
  • ▶️ Vite JS
  • ▶️ TypeScript
  • ▶️ React Query
  • ▶️ Rick and Morty API
  • ▶️ CSS vanilla (Los estilos los encuentras en el repositorio al final de este post)

 

Installation 🧰

  1. Clone the repository (you need to have Git installed).
    git clone https://github.com/Franklin361/infinite-scroll
Enter fullscreen mode Exit fullscreen mode
  1. Install dependencies of the project.
    npm install
Enter fullscreen mode Exit fullscreen mode
  1. Run the project.
    npm run dev
Enter fullscreen mode Exit fullscreen mode

 

Links ⛓️

Demo of the application 🔥

Here's the link to the tutorial in case you'd like to take a look at it! eyes 👀

  • 🇲🇽 🔗

  • 🇺🇲 🔗

Top comments (1)

Collapse
flash010603 profile image
Usuario163

Increible tutorial, buen trabajo!

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.