DEV Community

Cover image for Creating an infinite scroll with React JS! ♾️
Franklin Martinez
Franklin Martinez

Posted on

Creating an infinite scroll with React JS! ♾️

This time we are going to implement an infinite scroll using React JS.

An application that implements infinite scroll consists of a layout that allows users to keep consuming a certain amount of information without any pause, as the content is automatically loaded as the user scrolls.

🚨 Note: This post requires you to know the basics of React with TypeScript (basic hooks).

Any kind of feedback is welcome, thank you and I hope you enjoy the article.🤗

Table of contents.

📌 Technologies to be used.

📌 Creating the project.

📌 First steps.

📌 Making the API request.

📌 Showing the cards.

📌 Making the infinite scroll.

📌 Refactoring.

📌 Conclusion.

📌 Live demo.

📌 Source code.

 

🎈 Technologies to be used.

  • ▶️ React JS (version 18)
  • ▶️ Vite JS
  • ▶️ TypeScript
  • ▶️ React Query
  • ▶️ Rick and Morty API
  • ▶️ CSS vanilla (You can find the styles in the repository at the end of this post)

 

🎈 Creating the project.

We will name the project: infinite-scroll (optional, you can name it whatever you like).

npm init vite@latest
Enter fullscreen mode Exit fullscreen mode

We create the project with Vite JS and select React with TypeScript.

Then we run the following command to navigate to the directory just created.

cd infinite-scroll
Enter fullscreen mode Exit fullscreen mode

Then we install the dependencies.

npm install
Enter fullscreen mode Exit fullscreen mode

Then we open the project in a code editor (in my case VS code).

code .
Enter fullscreen mode Exit fullscreen mode

 

🎈 First steps.

First in the src/App.tsx file we will delete the content and add a title.

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

title

Next, we are going to create two components that we are going to use later. We create the folder src/components and inside we create the following files:

  • Loading.tsx

This file will contain the following:

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

It will be used to show a spinner when a new request is made to the API.

  • Card.tsx

This file will contain the following:

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

This is the card that will show the Rick and Morty API character.

In the src/interfaces folder we create an index.ts file and add the following 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

🚨 Note: The Result interface actually has more properties but in this case I will only use the ones I have defined.

 

🎈 Making the API request.

In this case we will use the React Query library that will allow us to perform the requests in a better way (and also has other features such as cache management).

  • Install the dependency
npm i @tanstack/react-query
Enter fullscreen mode Exit fullscreen mode

And then in the src/main.tsx file we are going to do the following:

We are going to enclose our App component inside the QueryClientProvider and send it the client which is just a new instance of 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

Now in the src/App.tsx file, we are going to use a special React Query hook called useInfiniteQuery.

const App = () => {

  useInfiniteQuery()

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

The useInfiniteQuery hook needs several parameters:

1 - queryKey: an array of strings or nested objects, which is used as a key to manage cache storage.

2 - queryFn: a function that returns a promise, the promise must be resolved or throw an error.

3 - options: within the options we need one called getNextPageParam which is a function that returns the information for the next query to the API.

The first parameter is the queryKey in this case we place an array with the word 'characters'.

const App = () => {

  useInfiniteQuery(
        ['characters']
    )

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

The second parameter is the queryFn in this case we place an array with the word 'characters'.

First we pass it a function

const App = () => {

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

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

This function must return a resolved promise.
For this, outside the component we create a function that will receive as parameter the page to fetch and will return a promise of type 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

The queryFn receives several parameters, among them the pageParam that by default will be undefined and then number, so if a value does not exist, we will equal it to 1, and this property we pass it to the function 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

Now the last parameter is the options, which is an object, which we will use the getNextPageParam property.

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

The function getNextPageParam receives two parameters but we will only use the first one that is the last page received (that is to say the last answer that the API gave us).

Inside the function, as the API of Rick and Morty does not come with the next page (rather it comes with the url for the next page) we will have to do the following:

1 - We will obtain the previous page.

The API response comes the info property that contains the prev property, we evaluate if it exists (because in the first call the prev property is null).

  • If it does not exist then it is page 0.
  • If it exists then we get that string we separate it and get the number.
const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0
Enter fullscreen mode Exit fullscreen mode

2 - We will get the current page.

We just add the previous page plus 1.

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

3 - We will evaluate if there are more pages.

We evaluate if the current page is equal to the total number of pages.

  • If it is true, then we return false so that it does not make another request.

  • If false, then we return the next page, which is the result of the sum of the current page plus 1.

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

return currentPage + 1;
Enter fullscreen mode Exit fullscreen mode

And this is how the hook would look like.

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

The hook useInfiniteQuery gives us certain values and functions of which we will use the following.

  • data: an object that contains the API query.

    • Inside this property there is another one called pages which is an array containing the obtained pages, from here we will get the API data.
  • error: An error message caused if the API request fails.

  • fetchNextPage: function that allows to make a new request to the next API page.

  • status: a string containing the values "error" | "loading" | "success" indicating the status of the request.

  • hasNextPage: a boolean value that is true if the getNextPageParam function returns a value that is not 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

 

🎈 Showing the cards.

Now we can show the results thanks to the fact that we already have access to the data.

We create a div and inside we are going to make an iteration on the data property accessing to the page property that is an array that for the moment we will access to the first position and to the results.

In addition we evaluate the status and if it is loading we show the component Loading.tsx but if it is in error, we place the error message.

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

This only shows the first page, next we will implement infinite scroll.

cards

 

🎈 Making the infinite scroll.

To do this we are going to use a popular library called react-infinite-scroll-component.

We install the dependency.

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

First we need the InfiniteScroll component.

<InfiniteScroll/>
Enter fullscreen mode Exit fullscreen mode

This component will receive several properties

  • dataLength: The number of elements, in a moment we will put the value since we need to calculate it.

  • next: a function that will be triggered when the end of the page is reached when scrolling. Here we will call the function that useInfiniteQuery offers us, fetchNextPage.

  • hasMore: boolean property that indicates if there are more elements. Here we will call the property offered by useInfiniteQuery, hasNextPage, and we convert it to boolean with !! because by default it is undefined.

  • loader: JSX component that will be used to display a loading message while the request is being made. Here we will call the Loading.tsx component.

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

Now, the property dataLength we could but this would only show the next page without accumulating the previous results, so we must do the following:

We will create a stored variable that will change every time the data property of useInfiniteQuery changes.

This characters variable must return a new ResponseAPI but the results property must accumulate the previous and current characters. And the info property will be that of the current page.

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

Now we pass this constant to dataLength, we make an evaluation if the characters exists then we place the length of the property results if not we place 0.

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

Now inside the component we must place the list to render, like this:

Now instead of iterating over data?.pages[0].results we are going to iterate over the memorized constant characters evaluating if it exists.

<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

And so everything would be complete:

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

And this is what it would look like.

Final

 

🎈 Refactoring.

Let's create a new folder src/hooks and add the useCharacter.ts file.

And we move all the logic.

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

Now in src/App.tsx it is easier to read.

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

 

🎈 Conclusion.

The whole process I just showed, is one of the ways you can implement infinite scroll in a fast way using third party packages. ♾️

I hope I helped you understand how to realize this design,thank you very much for making it this far! 🤗❤️

I invite you to comment if you find this article useful or interesting, or if you know any other different or better way of how to implement an infinite scroll. 🙌

 

🎈 Live demo.

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

 

🎈 Source code.

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

Collapse
 
flash010603 profile image
Usuario163

Thank you for sharing this information, it will be very useful for me!

Collapse
 
jonrandy profile image
Jon Randy 🎖️

The problem with this infinite scroll implementation (which unfortunately is an issue with MOST implementations of the feature) is that it is lazy and has no regard at all for the amount of memory or resources it uses up. What is the point of having every single last item that has been loaded always in the page? When you are a long way down the list there could be literally many thousands of elements in the page that are essentially no longer going to be used/viewed - yet they are still there taking up resources.