Creating an infinite scroll with React JS! ♾️
Tags: javascript, react, tutorial, webdev
Published: 2022-08-05 13:27:35

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.](#2)   
> 📌 [Creating the project.](#3)   
> 📌 [First steps.](#4)   
> 📌 [Making the API request.](#5)   
> 📌 [Showing the cards.](#6)   
> 📌 [Making the infinite scroll.](#7)   
> 📌 [Refactoring.](#8)   
> 📌 [Conclusion.](#9)   
>> 📌 [Live demo.](#10)   
>> 📌 [Source code.](#11)   

&nbsp;


## 🎈 Technologies to be used. <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 (You can find the styles in the repository at the end of this post) 

&nbsp;


## 🎈 Creating the project. <a id="3"></a>

We will name the project: **`infinite-scroll`** (optional, you can name it whatever you like).
```bash
npm init vite@latest
```
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.

```bash
cd infinite-scroll
```
Then we install the dependencies.
```bash
npm install
```
Then we open the project in a code editor (in my case VS code).
```bash
code .
```

&nbsp;


## 🎈 First steps. <a id="4"></a>

First in the `src/App.tsx` file we will delete the content and add a title.
```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)

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:
```js
export const Loading = () => {
    return (
        <div className="container-loading">
            <div className="spinner"></div>
            <span>Loading more characters...</span>
        </div>
    )
}
```
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:
```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>
    )
}
```
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.
```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;
}
``` 

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

&nbsp;


## 🎈 Making the API request. <a id="5"></a>

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

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

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

```

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

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

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

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

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

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


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


First we pass it a function 
```tsx
const App = () => {
     
  useInfiniteQuery(
        ['characters'],
        () => {}
    )

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

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

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

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

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


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

```tsx
const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0
```
**2 - We will get the current page**.

We just add the previous page plus 1.
```tsx
const currentPage = previousPage + 1;
```

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


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

return currentPage + 1;
```

And this is how the hook would look like.


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

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


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


## 🎈 Showing the cards. <a id="6"></a>

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.

```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
```
This only shows the first page, next we will implement infinite scroll.

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

&nbsp;


## 🎈 Making the infinite scroll. <a id="7"></a>

To do this we are going to use a popular library called [react-infinite-scroll-component](https://www.npmjs.com/package/react-infinite-scroll-component).

We install the dependency.
```
npm i react-infinite-scroll-component
```

First we need the **InfiniteScroll** component.
```tsx
<InfiniteScroll/>
```

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.


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

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.

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

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.

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

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.

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

And so everything would be complete:

```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
```
And this is what it would look like.

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

&nbsp;


## 🎈 Refactoring. <a id="8"></a>
Let's create a new folder `src/hooks` and add the **useCharacter.ts** file.


And we move all the logic.

```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
    }
}
```
**Now in `src/App.tsx` it is easier to read.**

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


## 🎈 Conclusion. <a id="9"></a>

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

&nbsp;

### 🎈 Live demo. <a id="10"></a>

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

&nbsp;

### 🎈 Source code. <a id="11"></a>
{%embed https://github.com/Franklin361/infinite-scroll %}