Create Infinite Scroll In React

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

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.

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

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 .

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

References

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

%%[donateviaupi]

Sun Feb 18 2024 09:14:04 GMT+0000 (Coordinated Universal Time)