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 hook2. 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
- Performance Optimization: Virtual scrolling for large datasets
- Intersection Observer: Efficient scroll detection
- State Management: Complex loading and error states
- User Experience: Smooth scrolling and loading indicators
- 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!
File Explorer - Machine Coding Question
Build a file explorer component using React with folder/file creation, navigation, and tree structure management
Job Board Component with Hacker News API
Master machine coding by building a job board component that fetches and displays job postings from the Hacker News API with pagination, loading states, and responsive design.