Infinite Scroll with Lazy Image Loading in React

Build performant, seamless content experiences using the Intersection Observer API and custom React hooks

Infinite scrolling has become a cornerstone of modern web interfaces, transforming how users consume content across social media feeds, e-commerce product galleries, and media-rich applications. This UX pattern eliminates the friction of pagination by dynamically loading content as users scroll, creating a seamless and engaging browsing experience. When combined with lazy image loading, these techniques work together to dramatically reduce initial page weight, speed up time-to-interactive, and conserve bandwidth.

The Intersection Observer API has revolutionized how developers implement these patterns, replacing inefficient scroll event listeners with a browser-native, performant solution. This approach offloads visibility detection to the browser, resulting in smoother scrolling, reduced CPU usage, and better overall user experience. For React developers, encapsulating this logic within custom hooks provides a clean, reusable foundation that can be applied across any project requiring infinite scroll functionality.

Our team specializes in creating performant user interfaces that leverage modern browser APIs to deliver exceptional experiences across all devices.

What Is Infinite Scrolling?

Infinite scrolling is a user interface pattern that automatically loads additional content when users approach the end of the currently visible content, creating the illusion of an endless stream. Instead of clicking "next page" or "load more" buttons, users simply continue scrolling, and new content seamlessly appears below. This pattern gained widespread adoption through social media platforms like Facebook, Twitter, and Instagram, where continuous content consumption is central to the user experience.

Common Use Cases

Use CaseDescription
Social Media FeedsFacebook, Twitter, Instagram load new posts while scrolling
E-commerce ProductsProduct grids auto-load more items as users explore
Search ResultsGoogle Images, YouTube load additional results dynamically
News & BlogsMedium, Dev.to load older articles as you scroll
DashboardsLarge tables and data logs with continuous loading

The mechanics rely on detecting when the user has scrolled near the bottom of the content list, then triggering an asynchronous fetch for the next batch of items. Once the new content arrives, it's appended to the existing list, extending the scrollable area and allowing the cycle to continue.

For a deeper dive into the fundamentals, see our guide on infinite scrolling best practices.

When to Use Infinite Scrolling

Infinite scrolling excels in scenarios where content is naturally organized in a linear stream and users benefit from frictionless exploration. However, it can complicate navigation when users need to return to specific content, create challenges for screen reader users, and introduce memory concerns with extremely long lists. Understanding when to apply this pattern ensures it enhances rather than hinders the user experience.

Why Lazy Loading Matters

Lazy loading is a performance optimization technique that defers the loading of non-critical resources until they're actually needed. For images, this means delaying the download and rendering of images that aren't yet visible in the viewport, only triggering the request when they approach the user's view.

Performance Benefits

  • Faster Initial Load: Only visible images load initially, reducing page weight dramatically
  • Reduced Bandwidth Consumption: Users download only images they actually view
  • Improved Core Web Vitals: Better LCP, FID, and CLS scores for SEO
  • Lower Server Costs: Fewer transferred resources at scale

Consider a product gallery displaying hundreds of items: loading all images simultaneously would create enormous network overhead. With lazy loading, only the images visible in the first viewport load initially. As users scroll, newly-visible images trigger their own loads, distributing network requests across time.

Combining Infinite Scroll with Lazy Loading

These patterns work together powerfully: infinite scroll manages the content loading flow while lazy loading ensures that within that content, images don't burden performance until they're actually viewed. This combination creates optimal experiences for both users and infrastructure.

Performance optimization is a core focus of our web development services, where we implement these patterns at scale for enterprise applications.

Understanding the Intersection Observer API

The Intersection Observer API represents a fundamental shift in how developers detect element visibility, replacing computationally expensive scroll event listeners with an efficient, browser-optimized mechanism. Rather than continuously calculating element positions relative to the viewport, Intersection Observer allows the browser to notify you only when a target element enters or exits a specified intersection area.

Key Concepts

  • Root Element: The viewport or scrollable container to compare against (defaults to viewport)
  • Target Element: The specific element whose visibility is being monitored
  • Thresholds: Percentage values indicating when to trigger callbacks (e.g., 0.1 = 10% visible)
  • RootMargin: Expands or contracts the intersection area for proactive triggering

Basic Implementation Pattern

const observer = new IntersectionObserver(([entry]) => {
 if (entry.isIntersecting) {
 loadMoreContent();
 }
}, {
 rootMargin: '200px' // Trigger 200px before element enters viewport
});

if (sentinelElement) {
 observer.observe(sentinelElement);
}

The rootMargin configuration proves particularly valuable for infinite scroll. By setting a positive margin, you trigger the load callback before the sentinel element actually enters the viewport, initiating content fetching slightly in advance and creating smoother experiences where new content often appears immediately as users scroll to the bottom.

Building a Custom Infinite Scroll Hook

A well-designed custom hook encapsulates the complexity of Intersection Observer setup, observation management, and cleanup, exposing a simple interface that components can use without understanding the underlying implementation.

The useInfiniteScroll Hook

import { useEffect, useRef } from "react";

interface UseInfiniteScrollProps {
 loadMore: () => void;
 hasMore: boolean;
}

const useInfiniteScroll = ({ loadMore, hasMore }: UseInfiniteScrollProps) => {
 const loadMoreRef = useRef<HTMLDivElement | null>(null);

 useEffect(() => {
 if (!hasMore) return;

 const observer = new IntersectionObserver(([entry]) => {
 if (entry.isIntersecting) {
 loadMore();
 }
 }, {
 rootMargin: '200px'
 });

 if (loadMoreRef.current) {
 observer.observe(loadMoreRef.current);
 }

 return () => observer.disconnect();
 }, [loadMore, hasMore]);

 return loadMoreRef;
};

export default useInfiniteScroll;

Component Integration

const ImageGallery = () => {
 const [visibleImages, setVisibleImages] = useState([]);
 const [hasMore, setHasMore] = useState(true);
 const [loading, setLoading] = useState(false);

 const loadMoreImages = useCallback(async () => {
 if (loading || !hasMore) return;
 setLoading(true);
 try {
 const newImages = await fetchNextBatch(visibleImages.length);
 setVisibleImages(prev => [...prev, ...newImages]);
 setHasMore(newImages.length > 0);
 } finally {
 setLoading(false);
 }
 }, [loading, hasMore, visibleImages.length]);

 const loadMoreRef = useInfiniteScroll({
 loadMore: loadMoreImages,
 hasMore
 });

 return (
 <div>
 <div className="image-grid">
 {visibleImages.map(image => (
 <LazyImage key={image.id} src={image.url} alt={image.alt} />
 ))}
 </div>
 <div ref={loadMoreRef} />
 {loading && <LoadingSpinner />}
 </div>
 );
};

For Vue implementations, explore our guide on implementing infinite scroll with Vue.

Lazy Loading Images in Infinite Scroll

Native Lazy Loading

Modern browsers support the loading attribute on image elements, providing a zero-JavaScript approach to lazy loading:

<img
 src="image.jpg"
 alt="Description"
 loading="lazy"
 width="800"
 height="600"
/>

Native lazy loading requires no JavaScript and works seamlessly within infinite scroll implementations. Images marked with loading="lazy" automatically defer their loads until scrolling brings them near the viewport.

Intersection Observer for Images

For finer control, implement custom lazy loading with Intersection Observer:

const useLazyImage = (src, threshold = 0.1) => {
 const [isLoaded, setIsLoaded] = useState(false);
 const [isInView, setIsInView] = useState(false);
 const imgRef = useRef(null);

 useEffect(() => {
 const observer = new IntersectionObserver(([entry]) => {
 if (entry.isIntersecting) {
 setIsInView(true);
 observer.disconnect();
 }
 }, { threshold });

 if (imgRef.current) {
 observer.observe(imgRef.current);
 }

 return () => observer.disconnect();
 }, [threshold]);

 return { imgRef, isLoaded, isInView, shouldLoad: isInView };
};

Placeholder Strategies

Effective placeholders balance performance and visual appeal:

  • Aspect Ratio Boxes: Reserve exact space before images load to prevent layout shift
  • Skeleton Loaders: Animated placeholders showing content shape
  • LQIP (Low-Quality Image Placeholders): Blurry preview that transitions to full image
<div className="image-container" style={{ aspectRatio: '4/3' }}>
 {isLoaded ? (
 <img src={realSrc} alt={alt} className="fade-in" onLoad={() => setIsLoaded(true)} />
 ) : (
 <div className="skeleton-loader" />
 )}
</div>

To explore more animation techniques, see our guide on CSS scroll-driven animations.

Performance Best Practices

Memory Management

Long-running infinite scroll implementations accumulate significant memory as items grow in the DOM. Consider these strategies:

  • Virtualization: Render only visible items with libraries like react-window
  • Cleanup Threshold: Remove items that scroll far out of view
  • Batch Limits: Cap maximum items before requiring page refresh
const CACHE_SIZE = 100;
const REMOVE_THRESHOLD = 50;

useEffect(() => {
 if (visibleImages.length > CACHE_SIZE + REMOVE_THRESHOLD) {
 setVisibleImages(prev => prev.slice(REMOVE_THRESHOLD));
 }
}, [visibleImages.length]);

Throttling and Debouncing

Prevent duplicate network requests with proper state management:

const loadMoreImages = useCallback(async () => {
 if (isLoading || !hasMore) return;

 setIsLoading(true);
 try {
 const newImages = await fetchImages(page + 1);
 setImages(prev => [...prev, ...newImages]);
 setPage(prev => prev + 1);
 } finally {
 setIsLoading(false);
 }
}, [isLoading, hasMore, page]);

Error Handling

Robust error handling ensures graceful recovery:

try {
 const newImages = await fetchImages(page + 1);
 setImages(prev => [...prev, ...newImages]);
} catch (error) {
 setIsError(true);
 setErrorMessage('Failed to load more images. Tap to retry.');
}

Provide retry mechanisms and clear error messaging for a positive user experience even when things go wrong.

Need help optimizing your application's performance? Our AI automation services can help implement intelligent caching and performance monitoring solutions.

User Experience Considerations

Loading States and Feedback

Clear, informative loading states transform potentially confusing waits into acceptable experiences:

<div ref={loadMoreRef}>
 {isLoading && (
 <div className="loading-indicator">
 <Spinner />
 <span>Loading more images...</span>
 </div>
 )}

 {isError && (
 <button onClick={retryLoad} className="error-state">
 <Icon name="refresh" />
 <span>{errorMessage}</span>
 </button>
 )}

 {!hasMore && images.length > 0 && (
 <div className="end-of-content">
 You've reached the end of the gallery
 </div>
 )}
</div>

Scroll Restoration

Handle browser back/forward navigation with scroll position storage:

useEffect(() => {
 const handleScroll = () => {
 sessionStorage.setItem('scrollPosition', window.scrollY);
 sessionStorage.setItem('itemsLoaded', visibleItems.length);
 };

 window.addEventListener('scroll', handleScroll, { passive: true });
 return () => window.removeEventListener('scroll', handleScroll);
}, [visibleItems.length]);

Accessibility

Ensure all users can effectively navigate infinite scroll content:

  • Use role="feed" for scrollable lists
  • Add aria-busy during loading states
  • Provide aria-live regions for status announcements
  • Include skip links and alternative navigation
  • Support keyboard navigation and focus management

Creating accessible, seamless experiences is central to our UI/UX design services.

Frequently Asked Questions

Ready to Build Seamless User Experiences?

Our UI/UX team specializes in creating performant, accessible interfaces that convert visitors into customers.