DEV Community

Cover image for Create Infinite scroll in React
Pratik sharma
Pratik sharma

Posted on • Updated on • Originally published at blog.coolhead.in

Create Infinite scroll in React

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Oldest comments (0)