avatar

Le Do Nghiem

Software Engineer

  • About me
  • Books
  • Snippets
  • Blog

© 2026 Le Do Nghiem. All rights reserved.

Contact |

Back to Snippets

useIntersectionObserver

Detect when an element enters or leaves the viewport using the Intersection Observer API.

LanguageTypeScript
Last UpdatedDec 21, 2025
use-intersection-observer.ts
import { useEffect, useRef, useState, RefObject } from 'react';

interface UseIntersectionObserverOptions extends IntersectionObserverInit {
  triggerOnce?: boolean;
}

interface UseIntersectionObserverReturn {
  ref: RefObject<HTMLElement>;
  isIntersecting: boolean;
  entry: IntersectionObserverEntry | null;
}

export function useIntersectionObserver(
  options: UseIntersectionObserverOptions = {}
): UseIntersectionObserverReturn {
  const { triggerOnce = false, ...observerOptions } = options;
  const [isIntersecting, setIsIntersecting] = useState(false);
  const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
  const elementRef = useRef<HTMLElement>(null);

  useEffect(() => {
    const element = elementRef.current;
    if (!element) return;

    const observer = new IntersectionObserver(([entry]) => {
      setIsIntersecting(entry.isIntersecting);
      setEntry(entry);

      if (triggerOnce && entry.isIntersecting) {
        observer.disconnect();
      }
    }, observerOptions);

    observer.observe(element);

    return () => {
      observer.disconnect();
    };
  }, [triggerOnce, observerOptions.root, observerOptions.rootMargin, observerOptions.threshold]);

  return { ref: elementRef, isIntersecting, entry };
}

The Intersection Observer API is perfect for lazy loading images, infinite scrolling, animations, and tracking visibility. This hook wraps the API in a React-friendly way.

import { useEffect, useRef, useState, RefObject } from 'react';

interface UseIntersectionObserverOptions extends IntersectionObserverInit {
  triggerOnce?: boolean;
}

interface UseIntersectionObserverReturn {
  ref: RefObject<HTMLElement>;
  isIntersecting: boolean;
  entry: IntersectionObserverEntry | null;
}

export function useIntersectionObserver(
  options: UseIntersectionObserverOptions = {}
): UseIntersectionObserverReturn {
  const { triggerOnce = false, ...observerOptions } = options;
  const [isIntersecting, setIsIntersecting] = useState(false);
  const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
  const elementRef = useRef<HTMLElement>(null);

  useEffect(() => {
    const element = elementRef.current;
    if (!element) return;

    const observer = new IntersectionObserver(([entry]) => {
      setIsIntersecting(entry.isIntersecting);
      setEntry(entry);

      if (triggerOnce && entry.isIntersecting) {
        observer.disconnect();
      }
    }, observerOptions);

    observer.observe(element);

    return () => {
      observer.disconnect();
    };
  }, [triggerOnce, observerOptions.root, observerOptions.rootMargin, observerOptions.threshold]);

  return { ref: elementRef, isIntersecting, entry };
}

How It Works

  • Creates an IntersectionObserver that watches the referenced element.
  • Updates state when the element enters or leaves the viewport.
  • Supports triggerOnce option to stop observing after first intersection.
  • Cleans up the observer on unmount.

Example Usage

function LazyImage({ src, alt }: { src: string; alt: string }) {
  const { ref, isIntersecting } = useIntersectionObserver({
    triggerOnce: true,
  });

  return (
    <div ref={ref}>
      {isIntersecting ? (
        <img src={src} alt={alt} />
      ) : (
        <div className="placeholder">Loading...</div>
      )}
    </div>
  );
}

Infinite Scroll Example

function InfiniteScroll({ onLoadMore }: { onLoadMore: () => void }) {
  const { ref, isIntersecting } = useIntersectionObserver({
    rootMargin: '100px',
  });

  useEffect(() => {
    if (isIntersecting) {
      onLoadMore();
    }
  }, [isIntersecting, onLoadMore]);

  return <div ref={ref}>Loading more...</div>;
}

Animation on Scroll

function AnimatedSection({ children }: { children: React.ReactNode }) {
  const { ref, isIntersecting } = useIntersectionObserver({
    threshold: 0.1,
  });

  return (
    <div
      ref={ref}
      className={`transition-opacity duration-500 ${
        isIntersecting ? 'opacity-100' : 'opacity-0'
      }`}
    >
      {children}
    </div>
  );
}

Use Cases

  • Lazy loading images
  • Infinite scrolling
  • Scroll-triggered animations
  • Analytics tracking (view impressions)
  • Sticky headers/navigation
  • Progress indicators

Notes

  • The observer is automatically cleaned up when the component unmounts.
  • triggerOnce is useful for one-time animations or lazy loading.
  • Adjust threshold to control when the callback fires (0.0 to 1.0).
  • rootMargin allows you to trigger before the element is fully visible.

A powerful hook that brings the Intersection Observer API into React, enabling performant scroll-based features.

Previous Snippet

useIsFirstRender Hook

Next Snippet

useCopyToClipboard Hook