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.
How to Implement React Infinite Scroll
One of the most common ways to implement React infinite scroll is through React libraries, following this code:
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;
We will explore how to implement infinite scroll in a React application, using its virtualization capabilities and optimizing performance.
There are three ways to implement infinite scrolling in React.
3 Ways to Implement React Infinite Scroll
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
:
npm install react-infinite-scroll-component axios
Or yarn
:
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 back end. 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.
The end of the fechMoreData
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>
);
The complete code would look like this:
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 to create 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>
);
};
The final 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);
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. Using the Intersection Observer API
Another way to implement infinite scroll by using 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 it:
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>
);
The complete code:
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 techniques, you can choose the one that best fits your project requirements and provide a smooth and engaging infinite scroll experience for your users.