Why Infinite Scroll Is Asked in Interviews
Infinite scroll is a very common frontend machine coding question because it tests multiple real-world skills at once. It involves handling user scroll events, managing API pagination, preventing redundant requests, handling loading states, and optimizing performance for large datasets.
Unlike simple UI questions, infinite scroll reveals whether a developer understands how the browser works, how to avoid unnecessary renders, and how to design scalable data fetching patterns.
What Problem Are We Solving?
Instead of loading thousands of items at once, which is slow and inefficient, infinite scroll loads data in chunks as the user scrolls. When the user reaches near the bottom of the list, the next batch of data is fetched automatically.
This pattern is widely used in social media feeds, product listings, search results, and content platforms.
Visualizing the User Experience
Imagine a user opening a feed that shows 10 items initially. As they scroll down and reach near the bottom, another 10 items are fetched and appended. The process continues seamlessly until there is no more data. The user never clicks a 'Load More' button, yet the list keeps growing naturally.
Core Requirements
- Load initial data when component mounts
- Detect when user reaches near bottom
- Fetch next page of data
- Append new data without replacing old data
- Prevent duplicate API calls
- Handle loading and end states
Step 1: State Design
A clean state structure is the foundation of a good solution. The component must track the list of items, current page, loading state, and whether more data exists.
const [items, setItems] = useState<any[]>([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);This structure ensures that UI, API calls, and scroll logic remain predictable.
Step 2: Fetching Data with Pagination
The API should support pagination. Each request fetches a specific page or offset of data.
const fetchData = async () => {
if (loading || !hasMore) return;
setLoading(true);
const res = await fetch(
`https://api.example.com/items?page=${page}`
);
const data = await res.json();
setItems((prev) => [...prev, ...data.items]);
if (data.items.length === 0) {
setHasMore(false);
} else {
setPage((prev) => prev + 1);
}
setLoading(false);
};Notice how new data is appended instead of replacing the old list. This is essential for infinite scroll behavior.
Step 3: Detecting Scroll Position
The key part of infinite scroll is detecting when the user has reached near the bottom of the page.
const handleScroll = () => {
const scrollTop = window.scrollY;
const windowHeight = window.innerHeight;
const fullHeight = document.body.scrollHeight;
if (scrollTop + windowHeight >= fullHeight - 200) {
fetchData();
}
};The buffer (e.g., 200px) ensures that data is fetched slightly before the user reaches the exact bottom, creating a smooth experience.
Step 4: Attaching Scroll Listener
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [loading, hasMore]);Cleaning up the event listener is important to avoid memory leaks and unexpected behavior.
Step 5: Initial Load
useEffect(() => {
fetchData();
}, []);This ensures the first set of data is loaded when the component mounts.
Complete Example Component
import React, { useEffect, useState } from "react";
export default function InfiniteScroll() {
const [items, setItems] = useState<any[]>([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const fetchData = async () => {
if (loading || !hasMore) return;
setLoading(true);
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=10`
);
const data = await res.json();
setItems((prev) => [...prev, ...data]);
if (data.length === 0) {
setHasMore(false);
} else {
setPage((prev) => prev + 1);
}
setLoading(false);
};
const handleScroll = () => {
if (
window.innerHeight + window.scrollY >=
document.body.offsetHeight - 200
) {
fetchData();
}
};
useEffect(() => {
fetchData();
}, []);
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () =>
window.removeEventListener("scroll", handleScroll);
}, [loading, hasMore]);
return (
<div>
{items.map((item, index) => (
<div key={index}>
<h3>{item.title}</h3>
</div>
))}
{loading && <p>Loading...</p>}
{!hasMore && <p>No more data</p>}
</div>
);
}How the Flow Works Internally
Initially, the first page is loaded. As the user scrolls down, the scroll event continuously checks position. When the threshold is reached, the next page is fetched and appended. This loop continues until the API returns no more data.
Better Approach: Intersection Observer
Using scroll events can be inefficient because they fire frequently. A more modern approach is using the Intersection Observer API to detect when a sentinel element becomes visible.
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
fetchData();
}
});This approach is more performant and avoids unnecessary calculations.
Common Mistakes
- Triggering multiple API calls due to missing loading guard
- Replacing instead of appending data
- Not handling end of data
- Using heavy scroll listeners without optimization
- Ignoring cleanup of event listeners
Performance Considerations
- Debounce or throttle scroll events
- Use Intersection Observer for better performance
- Implement virtualization for large lists
- Avoid unnecessary re-renders
Real-World Example
In an e-commerce application, products are loaded in batches of 20. As the user scrolls, more products load automatically. If the user reaches the end of available products, a 'No more products' message appears. This improves engagement compared to pagination because the user continues browsing without interruption.
Final Takeaway
Infinite scroll is not just about detecting scroll position. It is about designing a smooth data-loading experience that scales efficiently. A strong implementation handles pagination, avoids redundant calls, manages state cleanly, and optimizes performance.
In machine coding rounds, the best solutions are those that combine correctness with real-world usability and performance awareness.