JS Guide
HomeQuestionsTopicsCompaniesResources
BookmarksSearch

Built for developers preparing for JavaScript, React & TypeScript interviews.

ResourcesQuestionsSupport
HomeQuestionsSearchProgress
HomeQuestionsreact
Prev

Learn the concept

Advanced Patterns

react
mid
machine-coding

Build an infinite scroll component in React using Intersection Observer that loads more items as the user scrolls, with loading states and race condition handling.

react
machine-coding
infinite-scroll
intersection-observer
pagination
performance
interview
Quick Answer

Infinite scroll uses Intersection Observer to detect when a sentinel element near the bottom of the list enters the viewport, triggering a fetch for the next page of data. A ref-based approach with proper cleanup prevents race conditions and duplicate requests.

Detailed Explanation

Core Mechanism: Place a sentinel <div> at the bottom of the list. Attach an IntersectionObserver to it. When the sentinel becomes visible (enters the viewport), fetch the next page of data and append it to the existing list.

Implementation Steps:

  1. State: items (array), page (number), loading (boolean), hasMore (boolean)
  2. Sentinel ref: A useRef attached to a div after the last item
  3. Observer setup: In a useEffect, create an IntersectionObserver watching the sentinel. On intersection, increment the page.
  4. Fetch effect: A separate useEffect triggered by page changes that fetches data and appends to items.
  5. Cleanup: Disconnect the observer on unmount and abort pending fetches.

Race Condition Prevention:

  • Use a loading guard: don't trigger a new fetch if one is in progress
  • Use AbortController to cancel stale requests when the component unmounts or the page changes
  • Use a ref for the loading state (not just state) since the observer callback captures stale closures

Intersection Observer Options:

  • root: null — observe relative to the viewport
  • rootMargin: '200px' — start loading 200px before the sentinel is visible (for smoother UX)
  • threshold: 0 — trigger as soon as any part of the sentinel is visible

Edge Cases:

  • Empty pages: API returns no items — set hasMore = false and stop observing
  • Fast scrolling: User scrolls past multiple pages — the loading guard prevents duplicate fetches
  • Network errors: Show a retry button instead of silently failing
  • Initial load: First page should load on mount, not require scrolling

Performance Optimizations:

  • Windowing: For very long lists (10,000+ items), combine with virtualization (react-window) to only render visible DOM nodes
  • Image lazy loading: Use loading="lazy" on images within list items
  • Memoization: Wrap list items in React.memo to prevent re-renders when new items are appended

Code Examples

Infinite scroll with Intersection Observer and race condition handlingJSX
import { useState, useEffect, useRef, useCallback } from 'react';

function useInfiniteScroll(fetchPage) {
  const [items, setItems] = useState([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);
  const loadingRef = useRef(false);
  const observerRef = useRef(null);

  // Fetch data when page changes
  useEffect(() => {
    const controller = new AbortController();
    let cancelled = false;

    async function load() {
      if (loadingRef.current || !hasMore) return;
      loadingRef.current = true;
      setLoading(true);

      try {
        const newItems = await fetchPage(page, controller.signal);
        if (!cancelled) {
          setItems(prev => [...prev, ...newItems]);
          if (newItems.length === 0) setHasMore(false);
        }
      } catch (err) {
        if (err.name !== 'AbortError') console.error(err);
      } finally {
        if (!cancelled) {
          setLoading(false);
          loadingRef.current = false;
        }
      }
    }

    load();
    return () => { cancelled = true; controller.abort(); };
  }, [page, fetchPage, hasMore]);

  // Sentinel ref callback for Intersection Observer
  const sentinelRef = useCallback((node) => {
    if (observerRef.current) observerRef.current.disconnect();
    if (!node) return;

    observerRef.current = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting && !loadingRef.current) {
          setPage(p => p + 1);
        }
      },
      { rootMargin: '200px' }
    );
    observerRef.current.observe(node);
  }, []);

  return { items, loading, hasMore, sentinelRef };
}

// Usage: Product listing with infinite scroll
const fetchProducts = async (page, signal) => {
  const res = await fetch(`/api/products?page=${page}&limit=20`, { signal });
  const data = await res.json();
  return data.products; // Returns [] when no more
};

function ProductList() {
  const { items, loading, hasMore, sentinelRef } = useInfiniteScroll(fetchProducts);

  return (
    <div>
      <h1>Products</h1>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gap: 16 }}>
        {items.map(product => (
          <div key={product.id} style={{ border: '1px solid #ddd', padding: 16, borderRadius: 8 }}>
            <img src={product.image} alt={product.name} loading="lazy"
                 style={{ width: '100%', height: 200, objectFit: 'cover' }} />
            <h3>{product.name}</h3>
            <p>${product.price}</p>
          </div>
        ))}
      </div>

      {/* Sentinel element — triggers next page load */}
      {hasMore && <div ref={sentinelRef} style={{ height: 1 }} />}

      {loading && <p style={{ textAlign: 'center', padding: 16 }}>Loading more...</p>}
      {!hasMore && items.length > 0 && (
        <p style={{ textAlign: 'center', padding: 16, color: '#888' }}>
          You've reached the end
        </p>
      )}
    </div>
  );
}

Real-World Applications

Use Cases

Social Media Feeds

Instagram, Twitter, and LinkedIn use infinite scroll to load posts seamlessly as users scroll through their feeds.

E-commerce Product Listings

Food delivery and shopping apps load more restaurants or products as the user scrolls down, avoiding the friction of explicit pagination buttons.

Chat Message History

Messaging apps like Slack load older messages when the user scrolls to the top of a conversation, using reverse infinite scroll.

Mini Projects

Image Gallery with Infinite Scroll

intermediate

Build an Unsplash-style image gallery that fetches and displays photos in a masonry grid with infinite scroll, loading skeletons, and error retry.

Virtualized Infinite List

advanced

Combine infinite scroll with react-window virtualization to handle 100,000+ items with constant memory usage and smooth scrolling.

Industry Examples

Swiggy

Infinite scroll is a common Swiggy machine coding pattern. Their restaurant listing page uses it to progressively load restaurants as users scroll.

Meta

Facebook's News Feed pioneered the modern infinite scroll pattern, loading stories progressively using Intersection Observer and virtualized rendering.

Resources

MDN - Intersection Observer API

docs

React Docs - Referencing Values with Refs

docs

MDN - AbortController

docs

Related Questions

How does list virtualization (windowing) work in React and when should you use it?

senior
performance

How would you design and implement a reusable Pagination component in React?

senior
patterns
Previous
Build an autocomplete/typeahead search component in React that fetches suggestions from an API with debouncing, keyboard navigation, and highlighted matching text.
Prev