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 :
- Using the Intersection observable api
- Create your own custom hook with
useRef
Go here - react-in-vew here
- react-cool-inview here
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 iffalse
, 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. ππ»
-
Social Media
Twitter β @biomathcode
Next Blog
π Create Paginated Rest api with prisma, next.js and postgres.
Top comments (0)