High-Performance Infinite Scroll Component

Master machine coding by creating an infinite scroll component with virtualization, intersection observer, and advanced performance optimization techniques.

Machine Coding Challenge: Infinite Scroll Component

Create a production-ready infinite scroll component that handles large datasets efficiently with virtualization, intersection observer, and advanced React patterns.


Problem Statement

Design and implement an infinite scroll component with the following requirements:

✅ Core Features (Must Have)

  • Load more content as user scrolls
  • Smooth scrolling experience
  • Loading states and error handling
  • Responsive design for different screen sizes
  • Efficient rendering of large lists

Advanced Features (Nice to Have)

  • Virtual scrolling for performance
  • Intersection Observer API
  • Preloading and caching strategies
  • Pull-to-refresh functionality
  • Scroll position restoration
  • Keyboard navigation support

UI/UX Requirements

  • Smooth animations and transitions
  • Loading spinners and skeleton screens
  • Error states with retry functionality
  • Empty state design
  • Accessibility compliance

🧠 Clarifying Questions & Scope

Functional Requirements

  • Data Source: Mock API or real API integration?
  • Page Size: How many items per page? (10, 20, 50?)
  • Total Items: Expected maximum number of items?
  • Item Height: Fixed or variable height items?
  • Caching: Should we cache loaded data?

Technical Constraints

  • Time Limit: 2-3 hours for implementation
  • Framework: React with hooks
  • Performance: Handle 10,000+ items smoothly
  • Browser Support: Modern browsers with Intersection Observer

Success Criteria

  • Smooth scrolling with 60fps
  • Memory efficient for large datasets
  • Proper error handling and edge cases
  • Clean, maintainable code structure

🏗️ High-Level Architecture

1. Component Structure

InfiniteScroll/
├── InfiniteScrollContainer/  # Main container with scroll logic
│   ├── VirtualList/         # Virtual scrolling implementation
│   │   └── VirtualItem/     # Individual virtualized item
│   ├── LoadingIndicator/    # Loading spinner/skeleton
│   ├── ErrorBoundary/       # Error handling
│   └── EmptyState/          # Empty state component
├── useInfiniteScroll/       # Custom hook for scroll logic
├── useIntersectionObserver/ # Intersection Observer hook
└── useVirtualization/       # Virtual scrolling hook

2. State Management Strategy

interface InfiniteScrollState {
  items: Item[];
  loading: boolean;
  error: string | null;
  hasMore: boolean;
  page: number;
  scrollPosition: number;
  virtualStartIndex: number;
  virtualEndIndex: number;
}

interface Item {
  id: string;
  title: string;
  description: string;
  image?: string;
  timestamp: Date;
}

3. Data Flow

Scroll Event → Intersection Observer → Load More Data → Update State → Re-render

Virtual Scrolling ← Performance Optimization ← Large Dataset Handling

🛠️ Step-by-Step Implementation

Step 1: Basic Infinite Scroll Setup

// components/InfiniteScroll.tsx
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useInfiniteScroll } from '../hooks/useInfiniteScroll';
import { LoadingSpinner } from './LoadingSpinner';
import { ErrorBoundary } from './ErrorBoundary';

interface InfiniteScrollProps {
  fetchData: (page: number) => Promise<Item[]>;
  renderItem: (item: Item, index: number) => React.ReactNode;
  pageSize?: number;
  threshold?: number;
}

const InfiniteScroll: React.FC<InfiniteScrollProps> = ({
  fetchData,
  renderItem,
  pageSize = 20,
  threshold = 100
}) => {
  const [items, setItems] = useState<Item[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [hasMore, setHasMore] = useState(true);
  const [page, setPage] = useState(1);

  const loadMore = useCallback(async () => {
    if (loading || !hasMore) return;

    setLoading(true);
    setError(null);

    try {
      const newItems = await fetchData(page);
      
      if (newItems.length < pageSize) {
        setHasMore(false);
      }

      setItems(prev => [...prev, ...newItems]);
      setPage(prev => prev + 1);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to load data');
    } finally {
      setLoading(false);
    }
  }, [fetchData, page, pageSize, loading, hasMore]);

  const { observerRef } = useInfiniteScroll({
    onIntersect: loadMore,
    threshold,
    enabled: hasMore && !loading
  });

  return (
    <div className="infinite-scroll-container">
      <div className="items-list">
        {items.map((item, index) => (
          <div key={item.id} className="item-wrapper">
            {renderItem(item, index)}
          </div>
        ))}
      </div>

      {/* Intersection Observer Target */}
      <div ref={observerRef} className="observer-target" />

      {/* Loading State */}
      {loading && (
        <div className="loading-container">
          <LoadingSpinner />
          <span>Loading more items...</span>
        </div>
      )}

      {/* Error State */}
      {error && (
        <div className="error-container">
          <p>Error: {error}</p>
          <button onClick={loadMore} className="retry-button">
            Retry
          </button>
        </div>
      )}

      {/* End of List */}
      {!hasMore && items.length > 0 && (
        <div className="end-message">
          <p>You've reached the end of the list!</p>
        </div>
      )}

      {/* Empty State */}
      {!loading && !error && items.length === 0 && (
        <div className="empty-state">
          <p>No items found</p>
        </div>
      )}
    </div>
  );
};

Step 2: Custom Hooks Implementation

// hooks/useInfiniteScroll.ts
import { useRef, useEffect, useCallback } from 'react';

interface UseInfiniteScrollOptions {
  onIntersect: () => void;
  threshold?: number;
  enabled?: boolean;
}

export function useInfiniteScroll({
  onIntersect,
  threshold = 100,
  enabled = true
}: UseInfiniteScrollOptions) {
  const observerRef = useRef<HTMLDivElement>(null);

  const handleIntersect = useCallback(
    (entries: IntersectionObserverEntry[]) => {
      const [entry] = entries;
      if (entry.isIntersecting && enabled) {
        onIntersect();
      }
    },
    [onIntersect, enabled]
  );

  useEffect(() => {
    const observer = new IntersectionObserver(handleIntersect, {
      rootMargin: `${threshold}px`,
      threshold: 0.1
    });

    const currentRef = observerRef.current;
    if (currentRef) {
      observer.observe(currentRef);
    }

    return () => {
      if (currentRef) {
        observer.unobserve(currentRef);
      }
    };
  }, [handleIntersect, threshold]);

  return { observerRef };
}

// hooks/useVirtualization.ts
import { useState, useEffect, useMemo } from 'react';

interface UseVirtualizationOptions {
  items: any[];
  itemHeight: number;
  containerHeight: number;
  overscan?: number;
}

export function useVirtualization({
  items,
  itemHeight,
  containerHeight,
  overscan = 5
}: UseVirtualizationOptions) {
  const [scrollTop, setScrollTop] = useState(0);

  const virtualItems = useMemo(() => {
    const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
    const endIndex = Math.min(
      items.length - 1,
      Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
    );

    return items.slice(startIndex, endIndex + 1).map((item, index) => ({
      ...item,
      index: startIndex + index,
      style: {
        position: 'absolute' as const,
        top: (startIndex + index) * itemHeight,
        height: itemHeight,
        width: '100%'
      }
    }));
  }, [items, itemHeight, containerHeight, scrollTop, overscan]);

  const totalHeight = items.length * itemHeight;

  return {
    virtualItems,
    totalHeight,
    setScrollTop
  };
}

Step 3: Virtual Scrolling Implementation

// components/VirtualInfiniteScroll.tsx
import React, { useRef, useCallback } from 'react';
import { useVirtualization } from '../hooks/useVirtualization';
import { useInfiniteScroll } from '../hooks/useInfiniteScroll';

interface VirtualInfiniteScrollProps {
  items: Item[];
  itemHeight: number;
  containerHeight: number;
  fetchData: (page: number) => Promise<Item[]>;
  renderItem: (item: Item, index: number) => React.ReactNode;
  loading: boolean;
  hasMore: boolean;
  onLoadMore: () => void;
}

const VirtualInfiniteScroll: React.FC<VirtualInfiniteScrollProps> = ({
  items,
  itemHeight,
  containerHeight,
  fetchData,
  renderItem,
  loading,
  hasMore,
  onLoadMore
}) => {
  const containerRef = useRef<HTMLDivElement>(null);

  const { virtualItems, totalHeight, setScrollTop } = useVirtualization({
    items,
    itemHeight,
    containerHeight,
    overscan: 10
  });

  const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
    const scrollTop = e.currentTarget.scrollTop;
    setScrollTop(scrollTop);
  }, [setScrollTop]);

  const { observerRef } = useInfiniteScroll({
    onIntersect: onLoadMore,
    threshold: 200,
    enabled: hasMore && !loading
  });

  return (
    <div
      ref={containerRef}
      className="virtual-scroll-container"
      style={{ height: containerHeight, overflow: 'auto' }}
      onScroll={handleScroll}
    >
      <div style={{ height: totalHeight, position: 'relative' }}>
        {virtualItems.map(({ index, style, ...item }) => (
          <div key={item.id} style={style}>
            {renderItem(item, index)}
          </div>
        ))}
      </div>

      {/* Intersection Observer for loading more */}
      <div
        ref={observerRef}
        style={{
          position: 'absolute',
          bottom: 0,
          height: '1px',
          width: '100%'
        }}
      />

      {loading && (
        <div className="loading-indicator">
          <LoadingSpinner />
        </div>
      )}
    </div>
  );
};

Step 4: Advanced Features

// hooks/useScrollRestoration.ts
import { useEffect, useRef } from 'react';

export function useScrollRestoration(key: string) {
  const scrollPositionRef = useRef(0);

  useEffect(() => {
    const savedPosition = sessionStorage.getItem(`scroll-${key}`);
    if (savedPosition) {
      scrollPositionRef.current = parseInt(savedPosition, 10);
    }

    return () => {
      sessionStorage.setItem(`scroll-${key}`, scrollPositionRef.current.toString());
    };
  }, [key]);

  const saveScrollPosition = (position: number) => {
    scrollPositionRef.current = position;
    sessionStorage.setItem(`scroll-${key}`, position.toString());
  };

  const restoreScrollPosition = () => {
    return scrollPositionRef.current;
  };

  return { saveScrollPosition, restoreScrollPosition };
}

// hooks/usePullToRefresh.ts
import { useState, useRef, useCallback } from 'react';

export function usePullToRefresh(onRefresh: () => Promise<void>) {
  const [isRefreshing, setIsRefreshing] = useState(false);
  const [pullDistance, setPullDistance] = useState(0);
  const startY = useRef(0);
  const currentY = useRef(0);

  const handleTouchStart = useCallback((e: TouchEvent) => {
    startY.current = e.touches[0].clientY;
  }, []);

  const handleTouchMove = useCallback((e: TouchEvent) => {
    currentY.current = e.touches[0].clientY;
    const distance = currentY.current - startY.current;
    
    if (distance > 0 && window.scrollY === 0) {
      e.preventDefault();
      setPullDistance(Math.min(distance * 0.5, 100));
    }
  }, []);

  const handleTouchEnd = useCallback(async () => {
    if (pullDistance > 50) {
      setIsRefreshing(true);
      await onRefresh();
      setIsRefreshing(false);
    }
    setPullDistance(0);
  }, [pullDistance, onRefresh]);

  return {
    isRefreshing,
    pullDistance,
    handleTouchStart,
    handleTouchMove,
    handleTouchEnd
  };
}

UI/UX Enhancements

Loading States and Skeletons

// components/LoadingSkeleton.tsx
const LoadingSkeleton: React.FC = () => {
  return (
    <div className="skeleton-item animate-pulse">
      <div className="skeleton-image bg-gray-200 rounded-lg h-32 w-full mb-3" />
      <div className="skeleton-title bg-gray-200 h-4 w-3/4 mb-2" />
      <div className="skeleton-description bg-gray-200 h-3 w-full mb-1" />
      <div className="skeleton-description bg-gray-200 h-3 w-2/3" />
    </div>
  );
};

// components/LoadingSpinner.tsx
const LoadingSpinner: React.FC = () => {
  return (
    <div className="flex items-center justify-center p-4">
      <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
    </div>
  );
};

Error Handling and Retry

// components/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
}

interface State {
  hasError: boolean;
  error?: Error;
}

class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('Error caught by boundary:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div className="error-boundary">
          <h2>Something went wrong</h2>
          <p>{this.state.error?.message}</p>
          <button onClick={() => this.setState({ hasError: false })}>
            Try again
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

Performance Optimizations

Memoization and Optimization

// Optimized item rendering with React.memo
const VirtualItem = React.memo<{ item: Item; index: number }>(({ item, index }) => {
  return (
    <div className="virtual-item">
      <img src={item.image} alt={item.title} className="item-image" />
      <h3 className="item-title">{item.title}</h3>
      <p className="item-description">{item.description}</p>
      <span className="item-timestamp">
        {new Date(item.timestamp).toLocaleDateString()}
      </span>
    </div>
  );
});

// Optimized scroll handler with throttling
import { throttle } from 'lodash';

const throttledScrollHandler = useCallback(
  throttle((scrollTop: number) => {
    setScrollTop(scrollTop);
  }, 16), // ~60fps
  []
);

Caching and Preloading

// hooks/useDataCache.ts
import { useRef, useCallback } from 'react';

export function useDataCache<T>() {
  const cache = useRef<Map<string, T>>(new Map());

  const get = useCallback((key: string) => {
    return cache.current.get(key);
  }, []);

  const set = useCallback((key: string, value: T) => {
    cache.current.set(key, value);
  }, []);

  const clear = useCallback(() => {
    cache.current.clear();
  }, []);

  return { get, set, clear };
}

// Preloading next page
const preloadNextPage = useCallback(async () => {
  if (hasMore && !loading) {
    const nextPageData = await fetchData(page + 1);
    // Store in cache for instant loading
    cache.set(`page-${page + 1}`, nextPageData);
  }
}, [fetchData, page, hasMore, loading]);

Key Takeaways

What You've Learned

  1. Performance Optimization: Virtual scrolling for large datasets
  2. Intersection Observer: Efficient scroll detection
  3. State Management: Complex loading and error states
  4. User Experience: Smooth scrolling and loading indicators
  5. Memory Management: Efficient rendering and cleanup

Common Pitfalls to Avoid

  • Memory Leaks: Always cleanup observers and event listeners
  • Poor Performance: Don't render all items at once
  • Inconsistent Loading: Handle race conditions and errors
  • Bad UX: Missing loading states and error handling
  • Accessibility Issues: Ensure keyboard navigation support

Next Steps

  • Implement advanced caching strategies
  • Add real-time data updates
  • Optimize for mobile performance
  • Add comprehensive analytics
  • Implement advanced filtering and search

This infinite scroll component demonstrates advanced React patterns and performance optimization techniques. The skills you've learned here are essential for building scalable, user-friendly applications that handle large datasets efficiently!