React: How to implement an infinite scroll
Two ways to implement an infinite scroll in React Hooks
You might have heard a lot about an infinite scroll. Nowadays, this technique is so popular in many applications which consume massive amounts of data, especially social media app like twitter, facebook, etc. Today, we are going to implement an infinite scroll with React Hooks.
What is infinite scroll?
Infinite Scroll is a technique that automatically adds the next page as the user scrolls down through content, more content is loaded. User don’t need to wait for pages to preload. So infinite scroll can provide user a better experience.
For example, first page is only loaded at first time but next page is loaded as soon as user scroll down through contents.
Implement
So, how do we implement an infinite scroll in React Hooks? I will introduce most common three ways except library. You can choose one of these.
Three Options
window.scroll
eventintersectionObserver
useRef
window.scroll event
I don’t recommend this. Scroll event listener listens all user’s scrolling. So scroll event occurs way too much. It might affects performance of an application in the end. So if you want to use window.scroll
event, you need to use library like lodash-throttle
for optimization.
IntersectionObserver
The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document’s viewport.
I think it is the most common and efficient way to implement infinite scrolling. But keep in mind if you want to concern about browser compatibility, you have to polyfill it.
The concept is that you create an empty div element with ref
inside a list container. The div element is our target
. So we can observe intersecting through target
. If target
is intersecting, fetch new data.
First, create custom hook called useFetch
If you fetch data from external api, it is very common pattern using custom hooks in order to handle loading or error state. In this example, I will use axios
.
import { useState, useEffect, useCallback } from "react";
import axios from "axios";
function useFetch(query, page) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [list, setList] = useState([]);
const sendQuery = useCallback(async () => {
try {
await setLoading(true);
await setError(false);
const res = await axios.get(url);
await setList((prev) => [...prev, ...res.data];
setLoading(false);
} catch (err) {
setError(err);
}
}, [query, page]);
useEffect(() => {
sendQuery(query);
}, [query, sendQuery, page]);
return { loading, error, list };
}
export default useFetch;
Second, implement infinite scrolling with custom hook
import useFetch from "hooks/useFetch";
function App() {
const [query, setQuery] = useState("");
const [page, setPage] = useState(1);
const { loading, error, list } = useFetch(query, page);
const loader = useRef(null);
const handleChange = (e) => {
setQuery(e.target.value);
};
const handleObserver = useCallback((entries) => {
const target = entries[0];
if (target.isIntersecting) {
setPage((prev) => prev + 1);
}
}, []);
useEffect(() => {
const option = {
root: null,
rootMargin: "20px",
threshold: 0
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
}, [handleObserver]);
return (
<div className="App">
<h1>Infinite Scroll</h1>
<h2>with IntersectionObserver</h2>
<input type="text" value={query} onChange={handleChange} />
<div>
{list.map((item, i) => (
<div key={i}>{item}</div>
))}
</div>
{loading && <p>Loading...</p>}
{error && <p>Error!</p>}
<div ref={loader} />
</div>
);
}
export default App;
useRef
I found this awesome youtube video when I searched to implement infinite scrolling. So I applies this way with custom hook.
The concept is that you give ref
to only last element conditionally. If ref
exists it means that it is a last element, so fetch new data.
implement infinite scroll with custom hook
You can use useFetch
custom hook which is the same above one.
import useFetch from "hooks/useFetch";
function App() {
const [query, setQuery] = useState("");
const [pageNum, setPageNum] = useState(1);
const { loading, error, list, hasMore } = useSearchBook(query, pageNum);
const observer = useRef(); // (*)
const lastBookElementRef = useCallback( // (*)
(node) => {
if (isLoading) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
setPageNum((prev) => prev + 1);
}
});
if (node) observer.current.observe(node);
},
[loading, hasMore]
);
const handleChange = (e) => {
setQuery(e.target.value);
setPageNum(1);
};
return (
<div className="App">
<h1>Infinite Scroll</h1>
<h2>with useRef</h2>
<input type="text" onChange={handleChange} value={query} />
{list.map((item, i) => {
const isLastElement = books.length === i + 1;
isLastElement ? (
<div key={i} ref={lastBookElementRef}>
{book}
</div>
) : (
<div key={i}>{book}</div>
)
})}
<div>{isLoading && "Loading..."}</div>
<div>{error && "Error..."}</div>
</div>
);
export default App;
Additional
If you want to remove duplicated values of fetched data, you can use new Set
. Set
returns only unique values.
setBooks((prev) => {
return [...new Set([...prev, ...res.data)])];
});
Conclusion
If you want to provide more responsive and better experience to users, you can try this modern technique to your application — infinite scrolling. It is very easy to implement but so powerful.