Simple React Infinite Scroll using Intersection Observer API

Pablo Garcia
3 min readDec 28, 2021

--

JavaScript and browsers’ APIs are constantly evolving to incorporate common web patterns that aim to increase UX. For example, Infinite Scroll, which reduces First Contentful Paint (FCP), Time to Interactive (TTI), Speed Index, and Total Blocking Time because there is less data to load at first.

Consider a web page that uses infinite scrolling. It uses a vendor-provided library to manage the advertisements placed periodically throughout the page, has animated graphics here and there, and uses a custom library that draws notification boxes and the like. Each of these has its own intersection detection routines [scroll, touchstart, …], all running on the main thread. The author of the web site may not even realize this is happening, since they may know very little about the inner workings of the two libraries they are using. As the user scrolls the page, these intersection detection routines are firing constantly during the scroll handling code, resulting in an experience that leaves the user frustrated with the browser, the web site, and their computer.
MDN

With ReactJS and the Intersection Observer API we can build a remarkably simple component to handle Infinite Scroll:

import PropTypes from "prop-types"
import { useEffect, useRef } from "react"
InfiniteScroll.propTypes = {
children: PropTypes.node,
onLoadMore: PropTypes.func,
hasMore: PropTypes.bool,
reverse: PropTypes.bool,
threshold: PropTypes.number, // 0 to 1
style: PropTypes.object
};
function InfiniteScroll({
children,
hasMore = true,
reverse = false,
threshold = 0.7, // 70% visible
onLoadMore = () => {},
style = {}
}) {
const rootSentinelRef = useRef()
const tergetSentinelRef = useRef()
/**
* This custom hook is defined below
*/
const hasMoreRef = useRefVariable(hasMore)
const onLoadMoreRef = useRefVariable(onLoadMore)
useEffect(() => {
const rootSentinel = rootSentinelRef.current
const targetSentinel = tergetSentinelRef.current
const observer = new IntersectionObserver(
async ([entry], observer) => {
if (entry.isIntersecting && hasMoreRef.current) {
onLoadMoreRef.current(entry, observer)
}
},
{
root: rootSentinel,
rootMargin: getMargin(rootSentinel),
threshold
}
)
observer.observe(targetSentinel)
return () => observer.unobserve(targetSentinel)
}, [threshold, hasMoreRef, onLoadMoreRef])
return (
<div
ref={rootSentinelRef}
style={{
overflow: "auto",
...(reverse && {
display: "flex",
flexDirection: "column-reverse"
}),
...style
}}
>
{children}
<span ref={tergetSentinelRef} />
</div>
);
}
/**
* This function simply gets the computed margin so we don't need
* the parent component to pass a margin.
*/
function getMargin(element) {
const computedStyles = window.getComputedStyle(element);
return [
"margin-top",
"margin-right",
"margin-bottom",
"margin-left"
]
.reduce(
(accum, prop) =>
`${accum} ${
computedStyles?.getPropertyValue(prop) ||
computedStyles?.[prop] ||
"0px"
}`,
""
)
.trim();
}
/**
* You can read more: https://medium.com/@pgarciacamou/how-to-avoid-running-effects-on-every-render-in-react-userefvariable-hook-ad6e4a5e0abb
*/
function useRefVariable(value) {
const ref = useRef()
ref.current = value
return ref
}

And to use it:

<InfiniteScroll
style={{ height: "300px" }}
onLoadMore={() => doStuff()}
reverse={true|false}
>
<ul>
{Array.from(Array(35)).map((_, index) => (
<li key={index}>{index}</li>
))}
</ul>
</InfiniteScroll>

How does it work?

Notice that there are two sentinels, one for the container element rootSentineland another at the end of the list targetSentinel. These two are used with the Intersection Observability API to detect when the targetSentinel is in the visible area.

The clever thing is that we can use flex to revert the list and thus the targetSentinel will be at the top of the list and can then load elements from the top instead of the bottom.

Power up

We need to show some type of loading indicator to our users, the easiest thing to do is to append a loading icon at the end of the list (or beginning if reversed) and show it whenever we call loadMore. You can then also show a “no more items to load” when there is nothing else to load.

Playground

--

--

Pablo Garcia
Pablo Garcia

Written by Pablo Garcia

Senior Engineer at Netflix, ex-Staff Architect 2 at PayPal. M.S. in Computer Science w/specialization in Computing Systems. B.Eng. in Computer Software.

No responses yet