Infinite scroll has become a popular technique in web development to provide a seamless and dynamic user experience when dealing with large sets of data. It allows users to scroll through content endlessly without explicit pagination or loading new pages. In this blog, we will explore how to implement infinite scroll in a React application, leveraging its virtualization capabilities and optimizing performance.
There are 3 ways to implement infinite scrolling in react.
1. Using react libraries
To start implementing infinite scroll in your React application, we need to install some dependencies. The most commonly used library for this purpose is react-infinite-scroll-component. You can install it using npm or yarn,
npm install react-infinite-scroll-component axios
or
yarn add react-infinite-scroll-component axios
After that we need to import the components,
import React, { useState, useEffect } from "react";
import InfiniteScroll from "react-infinite-scroll-component";
Set the initial states of our component. This includes a list of items, load flags, and variables that store the index of the next page.
import React, { useState, useEffect } from "react";
import InfiniteScroll from "react-infinite-scroll-component";
import axios from "axios";
const InfiniteScrollExample1 = () => {
const [items, setItems] = useState([]);
const [hasMore, setHasMore] = useState(true);
const [index, setIndex] = useState(2);
// Rest of the components
Now let's see how the data is fetched from the backend. Here we are using Axios library and Platzi Fake Store for fetch dummy data.
So there are two parts in the code. First, using the useEffect
hook, we retrieve the initial product set from the API and update the items
state variable with the resolved API response's data.
The second part, fetchMoreData
function is defined separately to handle fetching more data when the user reaches the end of the page or triggers a specific event.
When the new data comes back, it adds it to the existing products in the items variable. It also checks if there are more products left to load, and if so, it sets a variable called hasMore
to true
, so we know we can load more later.
And at the end of the function updates the index
state.
useEffect(() => {
axios
.get("https://api.escuelajs.co/api/v1/products?offset=10&limit=12")
.then((res) => setItems(res.data))
.catch((err) => console.log(err));
}, []);
const fetchMoreData = () => {
axios
.get(`https://api.escuelajs.co/api/v1/products?offset=${index}0&limit=12`)
.then((res) => {
setItems((prevItems) => [...prevItems, ...res.data]);
res.data.length > 0 ? setHasMore(true) : setHasMore(false);
})
.catch((err) => console.log(err));
setIndex((prevIndex) => prevIndex + 1);
};
Then wrap the list of items in the InfiniteScroll component. Configure the component by passing the necessary props like dataLength, next, hasMore, and loader
return (
<InfiniteScroll
dataLength={items.length}
next={fetchMoreData}
hasMore={hasMore}
loader={<Loader />}
>
<div className='container'>
<div className='row'>
{items &&
items.map((item) => <ProductCard data={item} key={item.id} />)}
</div>
</div>
</InfiniteScroll>
);
And so everything would be complete:
import React, { useState, useEffect } from "react";
import InfiniteScroll from "react-infinite-scroll-component";
import axios from "axios";
import ProductCard from "./ProductCard";
import Loader from "./Loader";
const InfiniteScrollExample1 = () => {
const [items, setItems] = useState([]);
const [hasMore, setHasMore] = useState(true);
const [index, setIndex] = useState(2);
useEffect(() => {
axios
.get("https://api.escuelajs.co/api/v1/products?offset=10&limit=12")
.then((res) => setItems(res.data))
.catch((err) => console.log(err));
}, []);
const fetchMoreData = () => {
axios
.get(`https://api.escuelajs.co/api/v1/products?offset=${index}0&limit=12`)
.then((res) => {
setItems((prevItems) => [...prevItems, ...res.data]);
res.data.length > 0 ? setHasMore(true) : setHasMore(false);
})
.catch((err) => console.log(err));
setIndex((prevIndex) => prevIndex + 1);
};
return (
<InfiniteScroll
dataLength={items.length}
next={fetchMoreData}
hasMore={hasMore}
loader={<Loader />}
>
<div className='container'>
<div className='row'>
{items &&
items.map((item) => <ProductCard data={item} key={item.id} />)}
</div>
</div>
</InfiniteScroll>
);
};
export default InfiniteScrollExample1;
2. Building a Custom Solution
If you prefer a custom solution, you can implement infinite scroll by handling the scroll event manually. Let's see the code
import React, { useState, useEffect, useCallback } from "react";
import axios from "axios";
import ProductCard from "./ProductCard";
import Loader from "./Loader";
const InfiniteScrollExample2 = () => {
const [items, setItems] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [index, setIndex] = useState(2);
// Rest of the component
We can define the fetchData function using the useCallback hook to handle data fetching
const fetchData = useCallback(async () => {
if (isLoading) return;
setIsLoading(true);
axios
.get(`https://api.escuelajs.co/api/v1/products?offset=${index}0&limit=12`)
.then((res) => {
setItems((prevItems) => [...prevItems, ...res.data]);
})
.catch((err) => console.log(err));
setIndex((prevIndex) => prevIndex + 1);
setIsLoading(false);
}, [index, isLoading]);
We can fetch the initial data using the useEffect hook
useEffect(() => {
const getData = async () => {
setIsLoading(true);
try {
const response = await axios.get(
"https://api.escuelajs.co/api/v1/products?offset=10&limit=12"
);
setItems(response.data);
} catch (error) {
console.log(error);
}
setIsLoading(false);
};
getData();
}, []);
Next, we handle the scroll event and call the fetchData function when the user reaches the end of the page
useEffect(() => {
const handleScroll = () => {
const { scrollTop, clientHeight, scrollHeight } =
document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 20) {
fetchData();
}
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [fetchData]);
Finally, render the list of items along with a loader component
return (
<div className='container'>
<div className='row'>
{items.map((item) => (
<ProductCard data={item} key={item.id} />
))}
</div>
{isLoading && <Loader />}
</div>
);
};
And so everything would be complete:
import React, { useState, useEffect, useCallback } from "react";
import axios from "axios";
import ProductCard from "./ProductCard";
import Loader from "./Loader";
const InfiniteScrollExample2 = () => {
const [items, setItems] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [index, setIndex] = useState(2);
const fetchData = useCallback(async () => {
if (isLoading) return;
setIsLoading(true);
axios
.get(`https://api.escuelajs.co/api/v1/products?offset=${index}0&limit=12`)
.then((res) => {
setItems((prevItems) => [...prevItems, ...res.data]);
})
.catch((err) => console.log(err));
setIndex((prevIndex) => prevIndex + 1);
setIsLoading(false);
}, [index, isLoading]);
useEffect(() => {
const getData = async () => {
setIsLoading(true);
try {
const response = await axios.get(
"https://api.escuelajs.co/api/v1/products?offset=10&limit=12"
);
setItems(response.data);
} catch (error) {
console.log(error);
}
setIsLoading(false);
};
getData();
}, []);
useEffect(() => {
const handleScroll = () => {
const { scrollTop, clientHeight, scrollHeight } =
document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 20) {
fetchData();
}
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [fetchData]);
return (
<div className='container'>
<div className='row'>
{items.map((item) => (
<ProductCard data={item} key={item.id} />
))}
</div>
{isLoading && <Loader />}
</div>
);
};
export default InfiniteScrollExample2;
3. Leveraging the Intersection Observer API
Another approach to implementing infinite scroll is by leveraging the Intersection Observer API.
The Intersection Observer API is a modern development technique that can detect when elements appear, thus triggering content loading for infinite scrolling.
The Intersection Observer API observes changes in the intersection of the target elements with the ancestor or view element, making it well suited for implementing infinite scroll.
Let's see how to implement
import React, { useState, useEffect, useRef, useCallback } from "react";
import axios from "axios";
const InfiniteScrollExample3 = () => {
const [items, setItems] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [index, setIndex] = useState(2);
const loaderRef = useRef(null);
// Rest of the code
We can fetch the initial data using the useEffect
hook
useEffect(() => {
const getData = async () => {
setIsLoading(true);
try {
const response = await axios.get(
"https://api.escuelajs.co/api/v1/products?offset=10&limit=12"
);
setItems(response.data);
} catch (error) {
console.log(error);
}
setIsLoading(false);
};
getData();
}, []);
The fetchData
function is a special type of function created with the useCallback
hook. It remembers its definition and changes only if its dependencies (in this case index
and isLoading
) change.
Its purpose is to handle additional data retrieval from the API when it is invoked.
const fetchData = useCallback(async () => {
if (isLoading) return;
setIsLoading(true);
axios
.get(`https://api.escuelajs.co/api/v1/products?offset=${index}0&limit=12`)
.then((res) => {
setItems((prevItems) => [...prevItems, ...res.data]);
})
.catch((err) => console.log(err));
setIndex((prevIndex) => prevIndex + 1);
setIsLoading(false);
}, [index, isLoading]);
The useEffect
hook is used to configure the intersection watcher, which monitors the visibility of the loading element in the viewport. When the loading item is displayed, indicating that the user has scrolled down, the fetchData
function is invoked to fetch additional data.
The cleanup function ensures that the loader item is not observed when the component is no longer in use to avoid unnecessary observation.
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
const target = entries[0];
if (target.isIntersecting) {
fetchData();
}
});
if (loaderRef.current) {
observer.observe(loaderRef.current);
}
return () => {
if (loaderRef.current) {
observer.unobserve(loaderRef.current);
}
};
}, [fetchData]);
Finally, render the list of items along with a loader component
return (
<div className='container'>
<div className='row'>
{items.map((item, index) => (
<ProductCard data={item} key={item.id} />
))}
</div>
<div ref={loaderRef}>{isLoading && <Loader />}</div>
</div>
);
And so everything would be complete:
import React, { useState, useEffect, useRef, useCallback } from "react";
import axios from "axios";
import ProductCard from "./ProductCard";
import Loader from "./Loader";
const InfiniteScrollExample3 = () => {
const [items, setItems] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [index, setIndex] = useState(2);
const loaderRef = useRef(null);
const fetchData = useCallback(async () => {
if (isLoading) return;
setIsLoading(true);
axios
.get(`https://api.escuelajs.co/api/v1/products?offset=${index}0&limit=12`)
.then((res) => {
setItems((prevItems) => [...prevItems, ...res.data]);
})
.catch((err) => console.log(err));
setIndex((prevIndex) => prevIndex + 1);
setIsLoading(false);
}, [index, isLoading]);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
const target = entries[0];
if (target.isIntersecting) {
fetchData();
}
});
if (loaderRef.current) {
observer.observe(loaderRef.current);
}
return () => {
if (loaderRef.current) {
observer.unobserve(loaderRef.current);
}
};
}, [fetchData]);
useEffect(() => {
const getData = async () => {
setIsLoading(true);
try {
const response = await axios.get(
"https://api.escuelajs.co/api/v1/products?offset=10&limit=12"
);
setItems(response.data);
} catch (error) {
console.log(error);
}
setIsLoading(false);
};
getData();
}, []);
return (
<div className='container'>
<div className='row'>
{items.map((item, index) => (
<ProductCard data={item} key={item.id} />
))}
</div>
<div ref={loaderRef}>{isLoading && <Loader />}</div>
</div>
);
};
export default InfiniteScrollExample3;
And this is what it would look like.
By exploring these two techniques, you can choose the one that best fits your project requirements and provide a smooth and engaging infinite scroll experience for your users.🎈
I hope you found this article enjoyable and insightful! If you liked it, please feel free to leave a comment and share your thoughts. Thank you!🥰
For a detailed reference and access to the complete source code, you can find the Git repository 👉 here
Top comments (3)
This is a very well-explained and neat post! Thanks for sharing this Vishnu!
Thank you so much🥰
React is awesome JS Library 👨🏻🏫
Thanks for sharing Vishnu.
Learn React for free - shorturl.at/fEFIW