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.
🎈 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
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
Luego instalamos las dependencias.
npm install
Después abrimos el proyecto en un editor de código (en mi caso VS code).
code .
🎈 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
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>
)
}
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>
)
}
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;
}
🚨 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
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
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'
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
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.
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.
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
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.
const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0
2 - Obtendremos la pagina actual
Solo sumamos la pagina anterior más 1.
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.
if ( currentPage === lastPage.info.pages) return false;
return currentPage + 1;
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
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
🎈 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
Esto solo muestra la primera pagina, lo siguiente sera implementar el scroll infinito.
🎈 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
Primero necesitamos el componente InfiniteScroll.
<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
<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.
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.
<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.
<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:
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.
🎈 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
}
}
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
🎈 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.
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!
Features ⚙️
- View cards.
- 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 🧰
- Clone the repository (you need to have Git installed).
git clone https://github.com/Franklin361/infinite-scroll
- Install dependencies of the project.
npm install
- Run the project.
npm run dev
Top comments (1)
Increible tutorial, buen trabajo!