Pratik Sharma
Coolhead

Coolhead

Create Infinite Scroll In React

Create Infinite Scroll In React

A guide to create infinite scroll in react using react hook.

Pratik Sharma
Β·Mar 5, 2022Β·

7 min read

Subscribe to my newsletter and never miss my upcoming articles

Listen to this article

Table of contents

  • Components
  • Code Walkthrough
  • References

Components

There are mainly three components of infinite scroll. Fetching data from the paginated api,

Maintaining the data state on the website and detecting user scroll.

Fetching

You can do fetching with Fetch Api or Axios. Your api should have pagination.

In this blog we are going to use the fetch API.

State management

You can start with using react useState. You might want to persist data in local storage or have more complex state management with libraries such as Recoil, Redux , Zustand etc.

Detecting user scroll πŸ‘€

Have a loading component at the end of you list. If the loading component is in view, we will fetch more data. If we have reached the last page of paginated api , we will stop fetching.

We will use react-infinite-scroll-hook in this blog.

There are other ways to do the same. Here are some :

Code Repo

  • Github :

infinite-scroll-react/infinite-scroll-with-react at master Β· pratiksharm/infinite-scroll-react


βš™οΈ How does this works?

Infinite scrolling works in much the same way that normal website browsing works, behind the scenes. Your browser requests some content, and a web server sends it back.

Infinite scroll often works automatically, loading new content when the reader reaches the bottom of the page or close to it. But there are also compromises. Some sites present a load more button at the bottom of their content. This still uses the same underlying technique to inject more content, but it acts manually instead.

Infinite scroll works in a very simple way. Fetch more data when the user is at the bottom of the webpage.

Usually here is how we do fetching in react.

const [data, setData] = React.useState([])

const fetcher = async(url) => {
    const res = await fetch(url)
  setData(res.body.items);
}

useEffect(() => {

  fetcher(url)

},[data])

When a user scrolls done at the bottom of the page. If the Loader component is in view of the user screen, we will fetch more data. The Loader component is at the last of the list view and will be send at the bottom, thus not in view, not fetching more data.

We will be using the Github’s users api . You can use any api which have pagination. There are two types of paginations that are mainly used.

  • Offset Pagination
  • Cursor-based pagination

You can find references at the bottom of the page to read more about them.

Let’s add more State and change the fetcher function to support pagination.

const [data, setData] = React.useState([])

const [since, setSince] = useState(0);     // βœ…

const [limit, setLimit] = useState(10);    // βœ…

const [loading, setLoading] = useState(false); // βœ…

const fetcher = async (url) => {
      setSince(since + limit);
    const response = await fetch(`https://api.github.com/users?since=${since}&per_page=${limit}`);
    const json = await response.json();
    setData((data) => [...data, ...json]);
}

useEffect(() => {

  fetcher(url)

},[data, loading]) // Maybe add since and limit here as well πŸ₯³

We will Toggle the loading state, so that we can fetch more data. We are also incrementing the since state by limit i.e. 10.


Code Walkthrough

πŸ“Œ i am also planning to write my next blog on how to make a paginated api with prisma, nextjs and postgres . So, if you are interested maybe follow me πŸ™ŒπŸ».

Setup

Go ahead open vscode, in the terminal .

run npx create-react-app in our terminal.

npx create-react-app infinite-scroll

Styles

add a bit of styles with good old css in the app.css file. Create a classname of .main for the list view and a .item for our items.

.main {
  min-height: 100vh;
  padding: 4rem 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.item {
  display: flex;
  width: 300px;
  flex-direction: row;
  justify-content: space-between;
  margin-bottom: 30px;
  border-bottom: 2px solid #eaeaea;
}

Here is how our src/app.js would look like :

import { useState } from 'react';
import './App.css';

function App() {

  return (
    <div className="App">
      <h2>List of github users</h2>
      <main className='main'>

                <div className="loader">
                          <h1>Loading...</h1>
            </div>

      </main>

    </div>
  );
}

export default App;

States

We will have a few useState .

  • Data β‡’ so that we can store data
  • since β‡’ offset for pagination
  • limit β‡’ the number of list items per page.
  • Loading β‡’ Loading element will be used for fetching. If it is true, then we will fetch more and if false, not fetching.
  • hasNextPage β‡’ For stopping the fetching when there are no more pages. or data from the api.
import { useState } from 'react';
import './App.css';

function App() {
    const [data, setData] = useState([]);

    const [since, setSince] = useState(0);
    const [limit, setLimit] = useState(10);

    const [loading, setLoading] = useState(false);

    const [hasNextPage, setHasNextPage] = useState(true);

return (
            // like above
)}

export default App;

Fetch function

const fetchmore = async (since) => {
  setLoading(true)
  setSince(since + limit);
  try {
    const response = await fetch(`https://api.github.com/users?since=${since}&per_page=${limit}`);
    const json = await response.json();
    setData((data) => [...data, ...json]);
  }
  catch(e) {
    console.log(e);
        return setHasNextPage(false);
  }
  finally {
    setLoading(false);
  } 
}

fetchmore will run whenever the loader component is in view.

Then we have a setSince which will set the number of offset that we want. For example in the first fetch request since value is 0, limit = 10, β‡’ fetching the first 10 users of Github. Similarly, in the second fetch request we will get the next 10 users of Github.

setData is storing all the data that we are fetching and we can render the data state in the JSX. So let’s do that.

return (
    <div className="App">
      <h2>List of github users</h2>
      <main className='main'>

      {data && data.map((item, index) => {
          return (
            <div key={index} className='item'>
              <p>{item && item.login }</p>
              <img src={item.avatar_url} width={100} height={100} alt={item.avatar_url} />
            </div>
          )
        })}
        {
          (loading || hasNextPage) && 
          <div className="loader" >
          <h1>Loading...</h1>
        </div>
        }

      </main>


    </div>
  );

Loader component will always be at the bottom inside the main Dom element.

Loader component

If you look at the last coding block we added a loader component. It looks like this

 {
          (loading || hasNextPage) && 
          <div className="loader" >
          <h1>Loading...</h1>
        </div>
        }

For detecting this component is in view or not we will use the react-infinite-scroll-hook . The hook provides pretty much everything that we will need for creating infinite-scroll. It uses the Observable Api to detect if the component is in view or not.

npm install react-infinite-scroll-hook

Updating the app.jsx . Our component will look like this.

import { useState } from 'react';
import './App.css';

import useInfiniteScroll from 'react-infinite-scroll-hook';

function App() {
  const [data, setData] = useState([]);

  const [since, setSince] = useState(0);
  const [limit, setLimit] = useState(10);

  const [loading, setLoading] = useState(false);

  const [hasNextPage, setHasNextPage] = useState(true);

  const fetchmore = async (since) => {

    setLoading(true)
    setSince(since + limit);
    try {
      const response = await fetch(`https://api.github.com/users?since=${since}&per_page=${limit}`);
      const json = await response.json();
     return  setData((data) => [...data, ...json]);
    }
    catch(e) {
      console.log(e);
      return setHasNextPage(false);
    }
    finally {
     return  setLoading(false);
    }

  }

  const [sentryRef] = useInfiniteScroll({
    loading, 
    hasNextPage: hasNextPage ,
    delayInMs:500,
    onLoadMore: () => {
      fetchmore(since);
    }
  })

  return (
    <div className="App">
      <h2>List of github users</h2>
      <main className='main'>
      {data && data.map((item, index) => {
          return (
            <div key={index} className='item'>
              <p>{item && item.login }</p>
              <img src={item.avatar_url} width={100} height={100} alt={item.avatar_url} />
            </div>
          )
        })}
        {
          (loading || hasNextPage) && 
          <div className="loader" ref={sentryRef}>
          <h1>Loading...</h1>
        </div>
        }

      </main>


    </div>
  );
}

export default App;

Let’s look at who the hook will work.

const [sentryRef] = useInfiniteScroll({
    loading, 
    hasNextPage: hasNextPage ,
    delayInMs:500,
    onLoadMore: () => {
      fetchmore(since);
    }
  })
return ({ (loading || hasNextPage) && 
          <div className="loader" ref={sentryRef}>
          <h1>Loading...</h1>
        </div>
});

Set the sentryRef to the loader component. This way the hook will detect if the component is in view or not.

onLoadMore will run whenever the loader component is in view. We provide fetchmore which will fetch more data .

delayInMs is the delay we want before running onLoadMore .

For error handling you can also use disabled . It will stop the hook.

const [isError, setIsError] = useState(false);

const fetchmore = async (since) => {
    setLoading(true)
    setSince(since + limit);
    try {
      const response = await fetch(`https://api.github.com/users?since=${since}&per_page=${limit}`);
      const json = await response.json();
     return  setData((data) => [...data, ...json]);
    }
    catch(e) {
      console.log(e);
      setIsError(true);
      return setHasNextPage(false);
    }
    finally {
     return  setLoading(false);
    }

  }

const [sentryRef] = useInfiniteScroll({
    loading, 
    hasNextPage: hasNextPage ,
    delayInMs:500,
        disabled: isError,
    onLoadMore: () => {
      fetchmore(since);
    }
  })
return ({ (loading || hasNextPage) && 
          <div className="loader" ref={sentryRef}>
          <h1>Loading...</h1>
        </div>
});

This is pretty much it.

If I have done anything wrong do let me know in the comments.

Feedbacks are appreciated ✨.

If you face any error or maybe wanna say hi βœ‹πŸ». Feel free to dm me. πŸ‘‡πŸ»

Next Blog

πŸ“Œ Create Paginated Rest api with prisma, next.js and postgres.

References

  1. Pagination prisma
  2. Pagination at slack
  3. react-infinite-scroll-hook

Did you find this article valuable?

Support Pratik Sharma by becoming a sponsor. Any amount is appreciated!

See recent sponsors |Β Learn more about Hashnode Sponsors
Β 
Share this

Impressum

You can connect with me on Twitter | LinkedIn || github