typescriptintermediate

React Intersection Observer Hook

Custom useIntersectionObserver hook for lazy loading, infinite scroll, and scroll animations.

typescript
'use client';
import { useEffect, useRef, useState, useCallback } from 'react';

interface UseIntersectionOptions {
  threshold?: number;
  rootMargin?: string;
  triggerOnce?: boolean;
}

export function useIntersectionObserver({
  threshold = 0,
  rootMargin = '0px',
  triggerOnce = false,
}: UseIntersectionOptions = {}) {
  const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
  const [node, setNode] = useState<Element | null>(null);

  const ref = useCallback((el: Element | null) => setNode(el), []);

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

    const observer = new IntersectionObserver(
      ([entry]) => {
        setEntry(entry);
        if (triggerOnce && entry.isIntersecting) {
          observer.unobserve(node);
        }
      },
      { threshold, rootMargin }
    );

    observer.observe(node);
    return () => observer.disconnect();
  }, [node, threshold, rootMargin, triggerOnce]);

  return { ref, entry, isIntersecting: entry?.isIntersecting ?? false };
}

// Lazy load component
function LazySection({ children }: { children: React.ReactNode }) {
  const { ref, isIntersecting } = useIntersectionObserver({
    triggerOnce: true,
    rootMargin: '200px',
  });

  return (
    <div ref={ref}>
      {isIntersecting ? children : <div className="h-64 animate-pulse bg-gray-800 rounded" />}
    </div>
  );
}

// Infinite scroll
function InfiniteList() {
  const [items, setItems] = useState<string[]>(Array.from({ length: 20 }, (_, i) => `Item ${i}`));
  const { ref, isIntersecting } = useIntersectionObserver({ rootMargin: '100px' });

  useEffect(() => {
    if (isIntersecting) {
      setItems((prev) => [
        ...prev,
        ...Array.from({ length: 10 }, (_, i) => `Item ${prev.length + i}`),
      ]);
    }
  }, [isIntersecting]);

  return (
    <div>
      {items.map((item) => <div key={item} className="p-4 border-b">{item}</div>)}
      <div ref={ref} className="h-4" />
    </div>
  );
}

// Scroll animation
function AnimatedCard({ children }: { children: React.ReactNode }) {
  const { ref, isIntersecting } = useIntersectionObserver({ threshold: 0.2, triggerOnce: true });
  return (
    <div ref={ref} className={`transition-all duration-700 ${
      isIntersecting ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'
    }`}>{children}</div>
  );
}

Use Cases

  • Lazy loading components when they enter viewport
  • Infinite scroll pagination
  • Scroll-triggered animations and transitions

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.