React Code Interview 002: Infinite Scrolling with Pagination
Exploring the power of IntersectionObserver, Infinite Scrolling with Pagination API
Following our IntersectionObserver 101 deep dive, let’s kick it up a notch: infinite scrolling with pagination. Instead of making users click "Load More," we can auto-fetch new data whenever they reach the bottom of a scrollable container—giving them a smooth, uninterrupted experience.
Why Go Infinite?
Streamlined UX: Continuous content keeps readers engaged—no pesky buttons.
Popular Interview Challenge: Recruiters often ask for infinite scrolling implementations to see if you can handle both front-end logic (scroll events, IntersectionObserver) and back-end pagination.
Practical Real-World Scenario: Think of those never-ending social media feeds or e-commerce product grids.
Core Code Snippet
Here’s a pared-down look at how we might fetch paginated quotes, observe a “trigger” element, and fetch the next page automatically:
import { useEffect, useRef, useState } from "react";
function App() {
const containerRef = useRef<HTMLDivElement | null>(null);
const triggerRef = useRef<HTMLLIElement | null>(null);
const [quoteList, setQuoteList] = useState<Quote[]>([]);
// Keep track of the current page & if more data is available
const [page, setPage] = useState<number>(1);
const [hasMore, setHasMore] = useState<boolean>(false);
const fetchNext = async () => {
const response = await fetch(`/quotes/paginated?page=${page}`);
const { meta, quotes } = await response.json();
setQuoteList(prev => [...prev, ...quotes]);
setPage(prev => prev + 1);
setHasMore(meta.hasMore);
};
useEffect(() => {
// Initial data fetch
fetchNext();
}, []);
useEffect(() => {
if (!containerRef.current) return;
const observerOptions = {
root: containerRef.current,
rootMargin: "0px",
threshold: 1.0,
};
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
fetchNext();
}
}, observerOptions);
if (triggerRef.current) observer.observe(triggerRef.current);
return () => observer.disconnect();
}, [page, hasMore]);
return (
<div className="container" ref={containerRef}>
<ol>
{quoteList.map((quote) => (
<li key={quote.id}>
{quote.text} - {quote.author}
</li>
))}
<li ref={triggerRef}>Load more...</li>
</ol>
</div>
);
}
export default App;
Key Points:
containerRef
: The scrollable element we treat as the viewport (instead of the document body).triggerRef
: A small (often invisible) element the observer will watch. Once it’s fully visible (threshold: 1.0
), we fetch more data.hasMore
&page
: Simple refs to track the pagination state and whether additional pages remain.
The Paginated JSON
Your backend likely returns an meta
object alongside the actual data array, like so:
{
"meta": {
"currentPage": "10",
"hasMore": false,
"pageSize": 5,
"total": 49
},
"quotes": [
{
"id": 46,
"text": "Do not let making a living prevent you from making a life.",
"author": "John Wooden"
},
...
]
}
hasMore
lets the client know when to stop fetching.currentPage
helps you keep track of how far into the list you are.
If you prefer to watch it in action, I have published a video recently to cover that, please check it out:
See It in Action
My Take: Balancing Complexity & Experience
Building an infinite scroll from scratch can feel daunting. You’ll manage states for loading, errors, and caching, and watch out for useEffect
triggering multiple times. But the smooth user experience makes it worthwhile. Plus, it’s great practice for real projects and technical interviews alike.
Pro Tip: Adjust rootMargin
to fetch the next set of data before users actually hit the bottom. For example, rootMargin: "300px"
will preload data sooner and avoid any awkward “blank space” moments in your feed.
What’s Next?
React Query: A more robust solution for data fetching, caching, and background updates.
Error & Loading States: Handling failure and showing spinners so users aren’t confused if something goes wrong.
Feel free to experiment, add personal touches (like spinners or error messages), and refine your approach. Let me know if infinite scrolling makes your dev life easier—or if you have any tips to share!
Thanks for reading,
Juntao Qiu
The Pragmatic Developer