Skip to content
intermediatePhase 37 · Frontend Architecture

Infinite Scroll

Implement infinite scrolling with intersection observers and virtual lists.

45m
0 problems
Topic Progress0%

Intersection Observer

Intersection Observer

Intersection Observer detects when elements enter or leave the viewport.

Basic Implementation

// hooks/useInfiniteScroll.js
function useInfiniteScroll(callback, options = {}) {
  const { threshold = 0.1, rootMargin = '100px' } = options;
  const observerRef = useRef(null);
  const sentinelRef = useRef(null);

  useEffect(() => {
    if (observerRef.current) observerRef.current.disconnect();

    observerRef.current = new IntersectionObserver(
      (entries) => {
        const [entry] = entries;
        if (entry.isIntersecting) {
          callback();
        }
      },
      { threshold, rootMargin }
    );

    if (sentinelRef.current) {
      observerRef.current.observe(sentinelRef.current);
    }

    return () => observerRef.current?.disconnect();
  }, [callback, threshold, rootMargin]);

  return sentinelRef;
}

With React Query

function InfiniteList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isLoading,
  } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: ({ pageParam = 0 }) =>
      fetch(`/api/posts?cursor=${pageParam}`).then(r => r.json()),
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  });

  const sentinelRef = useInfiniteScroll(
    () => fetchNextPage(),
    { rootMargin: '200px' }
  );

  const allPosts = data?.pages.flatMap(page => page.items) || [];

  if (isLoading) return <Spinner />;

  return (
    <div className="infinite-list">
      {allPosts.map(post => (
        <article key={post.id} className="post-card">
          <h3>{post.title}</h3>
          <p>{post.excerpt}</p>
        </article>
      ))}
      
      {hasNextPage && (
        <div ref={sentinelRef} className="sentinel">
          {isFetchingNextPage && <Spinner />}
        </div>
      )}
    </div>
  );
}

Custom Hook with Options

function useInfiniteScrollList({ queryKey, fetchFn, getNextCursor }) {
  const [items, setItems] = useState([]);
  const [cursor, setCursor] = useState(null);
  const [hasMore, setHasMore] = useState(true);
  const [isLoading, setIsLoading] = useState(false);

  const loadMore = useCallback(async () => {
    if (isLoading || !hasMore) return;
    
    setIsLoading(true);
    try {
      const response = await fetchFn(cursor);
      setItems(prev => [...prev, ...response.items]);
      setCursor(getNextCursor(response));
      setHasMore(response.hasMore);
    } finally {
      setIsLoading(false);
    }
  }, [cursor, hasMore, isLoading, fetchFn, getNextCursor]);

  const sentinelRef = useInfiniteScroll(loadMore);

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

Scroll Restoration

function InfiniteListWithScrollRestore() {
  const [scrollPosition, setScrollPosition] = useState(0);
  const listRef = useRef(null);

  // Save scroll position
  useEffect(() => {
    const handleScroll = () => {
      sessionStorage.setItem('scrollPosition', window.scrollY);
    };
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);

  // Restore scroll position
  useEffect(() => {
    const savedPosition = sessionStorage.getItem('scrollPosition');
    if (savedPosition) {
      window.scrollTo(0, parseInt(savedPosition));
    }
  }, []);

  // Rest of component...
}

Virtual Lists

Virtual Lists

Virtualization renders only visible items, enabling smooth scrolling of thousands of items.

Basic Virtual List

// components/VirtualList.jsx
function VirtualList({ items, itemHeight = 50, containerHeight = 400 }) {
  const [scrollTop, setScrollTop] = useState(0);

  const startIndex = Math.floor(scrollTop / itemHeight);
  const endIndex = Math.min(
    startIndex + Math.ceil(containerHeight / itemHeight) + 1,
    items.length
  );

  const visibleItems = items.slice(startIndex, endIndex);
  const totalHeight = items.length * itemHeight;
  const offsetY = startIndex * itemHeight;

  return (
    <div
      className="virtual-list"
      style={{ height: containerHeight, overflow: 'auto' }}
      onScroll={(e) => setScrollTop(e.target.scrollTop)}
    >
      <div style={{ height: totalHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item, index) => (
            <div
              key={item.id}
              style={{ height: itemHeight }}
              className="virtual-list-item"
            >
              {item.content}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

Using react-window

import { FixedSizeList } from 'react-window';

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style} className="list-item">
      <span>{items[index].name}</span>
    </div>
  );

  return (
    <FixedSizeList
      height={400}
      width="100%"
      itemCount={items.length}
      itemSize={50}
    >
      {Row}
    </FixedSizeList>
  );
}

Variable Size Lists

import { VariableSizeList } from 'react-window';

function VariableVirtualList({ items }) {
  const listRef = useRef(null);

  const getItemSize = (index) => {
    const item = items[index];
    return item.expanded ? 150 : 60;
  };

  const Row = ({ index, style }) => (
    <div style={style} className="list-item">
      <h4>{items[index].title}</h4>
      {items[index].expanded && (
        <p>{items[index].content}</p>
      )}
    </div>
  );

  return (
    <VariableSizeList
      ref={listRef}
      height={400}
      width="100%"
      itemCount={items.length}
      itemSize={getItemSize}
    >
      {Row}
    </VariableSizeList>
  );
}

Performance Considerations

Optimization Tips:

  • Use fixed item heights when possible
  • Avoid expensive computations in row rendering
  • Use memo for row components
  • Implement overscan (render extra items)
  • Consider windowing for 1000+ items

Performance Considerations

Performance Considerations

Memory Management

// Clean up old items to prevent memory leaks
function useInfiniteScrollMemory(items, setItems, maxItems = 500) {
  useEffect(() => {
    if (items.length > maxItems) {
      const itemsToRemove = items.length - maxItems;
      setItems(prev => prev.slice(itemsToRemove));
    }
  }, [items.length, setItems, maxItems]);
}

Debounced Loading

function useDebouncedInfiniteScroll(callback, delay = 100) {
  const timeoutRef = useRef(null);
  const isLoadingRef = useRef(false);

  const debouncedCallback = useCallback(() => {
    if (isLoadingRef.current) return;

    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }

    timeoutRef.current = setTimeout(async () => {
      isLoadingRef.current = true;
      await callback();
      isLoadingRef.current = false;
    }, delay);
  }, [callback, delay]);

  return debouncedCallback;
}

Pre-fetching

function usePrefetchOnScroll({ queryKey, fetchFn, threshold = 0.8 }) {
  const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
    queryKey,
    queryFn: fetchFn,
  });

  useEffect(() => {
    const handleScroll = () => {
      const scrollPercentage = 
        window.scrollY / (document.body.scrollHeight - window.innerHeight);
      
      if (scrollPercentage > threshold && hasNextPage) {
        fetchNextPage();
      }
    };

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

Error Recovery

function InfiniteListWithErrorRecovery() {
  const [retryCount, setRetryCount] = useState(0);
  const maxRetries = 3;

  const {
    data,
    error,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isError,
  } = useInfiniteQuery({
    queryKey: ['items', retryCount],
    queryFn: fetchItems,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
    retry: false,
  });

  const handleRetry = () => {
    if (retryCount < maxRetries) {
      setRetryCount(prev => prev + 1);
    }
  };

  if (isError && retryCount >= maxRetries) {
    return (
      <div>
        <p>Failed to load items</p>
        <button onClick={() => window.location.reload()}>
          Reload Page
        </button>
      </div>
    );
  }

  return (
    <div>
      {data?.pages.map((page, i) => (
        <div key={i}>
          {page.items.map(item => (
            <Item key={item.id} item={item} />
          ))}
        </div>
      ))}
      
      {hasNextPage && (
        <button
          onClick={() => fetchNextPage()}
          disabled={isFetchingNextPage}
        >
          {isFetchingNextPage ? 'Loading...' : 'Load More'}
          {isError && ' (Click to retry)'}
        </button>
      )}
    </div>
  );
}

Performance Checklist

  • Implement virtualization for 1000+ items
  • Use passive scroll listeners
  • Debounce scroll handlers
  • Clean up old items from memory
  • Pre-fetch next page before reaching end
  • Handle errors with retry logic
  • Show loading indicators

Practice Problems

0/3solved
Build Infinite Scroll Component

Create a reusable React component implementing Infinite Scroll. Include proper state management and accessibility.

Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where needed
Infinite Scroll Testing

Write unit and integration tests for Infinite Scroll using React Testing Library.

Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility tests
Infinite Scroll Performance

Optimize Infinite Scroll for performance. Consider memoization, code splitting, and bundle size.

Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysis

Quiz

1. What does Intersection Observer detect?

Question 1 options

2. When should you use virtualization?

Question 2 options

3. What is overscan in virtual lists?

Question 3 options

4. Why use passive scroll listeners?

Question 4 options

5. What should you do to prevent memory leaks in infinite scroll?

Question 5 options

Flashcards

Question

What is Intersection Observer?

Answer

An API that detects when elements enter or leave the viewport, used for infinite scroll triggers.

Question

What is virtualization?

Answer

Rendering only visible items in a list to reduce DOM nodes and improve performance.

Question

What is overscan?

Answer

Extra items rendered outside the viewport for smoother scrolling experience.

Question

Why use passive scroll listeners?

Answer

They don't block scrolling, providing better performance for scroll event handling.

Question

What is Infinite Scroll?

Answer

Infinite Scroll is a key concept in frontend development.

Revision Notes

Key Takeaways

  • 1.Intersection Observer is the modern way to detect scroll position
  • 2.Virtualization is essential for lists with thousands of items
  • 3.Always use passive scroll listeners for better performance
  • 4.Pre-fetch next pages before users reach the end
  • 5.Clean up old items to prevent memory leaks

Interview Tips

  • Explain how Intersection Observer works and its use cases
  • Discuss when virtualization is necessary
  • Know the performance implications of infinite scroll

Cheat Sheet

Infinite Scroll Cheat Sheet

Intersection Observer

const observer = new IntersectionObserver(callback, {
  threshold: 0.1,
  rootMargin: '100px'
});
observer.observe(sentinel);

Virtual List

  • Render only visible items
  • Use react-window or react-virtualized
  • Set fixed item heights for best performance
  • Add overscan for smooth scrolling

Performance Tips

  • Use passive scroll listeners
  • Debounce scroll handlers
  • Pre-fetch next page
  • Clean up old items from memory