← Back to all hooks

useIntersectionObserver

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

Approach 1: Multiple Observers

Each element gets its own observer instance. Simple and independent tracking.

Scroll inside the box to see multiple elements being tracked independently

↓ Scroll down ↓

Box 1: ❌ Hidden

Box 2: ❌ Hidden

Box 3: ❌ Hidden

↑ Scroll up ↑

Approach 2: Single Observer

One observer tracks multiple elements. More efficient for many elements.

Single Observer tracking multiple elements

More efficient for tracking many elements

↓ Scroll down ↓

Box 1: ❌ Hidden

Box 2: ❌ Hidden

Box 3: ❌ Hidden

↑ Scroll up ↑

Code Examples

Multiple Observers (Simple)

import { useIntersectionObserver } from "@uiblock/hooks";

export default function Demo() {
  // Each call creates a separate observer
  const [ref1, isVisible1] = useIntersectionObserver();
  const [ref2, isVisible2] = useIntersectionObserver();
  const [ref3, isVisible3] = useIntersectionObserver();

  return (
    <div>
      <div ref={ref1}>Box 1: {isVisible1 ? 'Visible' : 'Hidden'}</div>
      <div ref={ref2}>Box 2: {isVisible2 ? 'Visible' : 'Hidden'}</div>
      <div ref={ref3}>Box 3: {isVisible3 ? 'Visible' : 'Hidden'}</div>
    </div>
  );
}

Single Observer (Efficient)

import { useIntersectionObserverMultiple } from "@uiblock/hooks";
import { useRef, useEffect } from "react";

export default function Demo() {
  const { observe, isVisible } = useIntersectionObserverMultiple();
  const box1Ref = useRef(null);
  const box2Ref = useRef(null);
  const box3Ref = useRef(null);

  useEffect(() => {
    if (box1Ref.current) observe(box1Ref.current, 'box1');
    if (box2Ref.current) observe(box2Ref.current, 'box2');
    if (box3Ref.current) observe(box3Ref.current, 'box3');
  }, [observe]);

  return (
    <div>
      <div ref={box1Ref}>Box 1: {isVisible('box1') ? 'Visible' : 'Hidden'}</div>
      <div ref={box2Ref}>Box 2: {isVisible('box2') ? 'Visible' : 'Hidden'}</div>
      <div ref={box3Ref}>Box 3: {isVisible('box3') ? 'Visible' : 'Hidden'}</div>
    </div>
  );
}

When to Use Each

Multiple Observers

  • Simple, independent tracking
  • Few elements (1-10)
  • Each element has unique logic
  • Easier to understand

Single Observer

  • Better performance
  • Many elements (10+)
  • Lazy loading lists/grids
  • Lower memory usage

How It Works

Here's the implementation:

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

export function useIntersectionObserver(options?) {
  const [isVisible, setIsVisible] = useState(false)
  const ref = useRef(null)

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

    const observer = new IntersectionObserver(([entry]) => {
      setIsVisible(entry.isIntersecting)
    }, options)

    observer.observe(element)

    return () => observer.disconnect()
  }, [options])

  return [ref, isVisible]
}

Multiple Elements Version

For tracking multiple elements with a single observer:

import { useEffect, useRef, useState, useCallback } from 'react'

export function useIntersectionObserverMultiple(options?) {
  const [visibilityMap, setVisibilityMap] = useState(new Map())
  const observerRef = useRef(null)

  useEffect(() => {
    observerRef.current = new IntersectionObserver((entries) => {
      setVisibilityMap((prev) => {
        const newMap = new Map(prev)
        entries.forEach((entry) => {
          const id = entry.target.getAttribute('data-observe-id')
          if (id) {
            newMap.set(id, entry.isIntersecting)
          }
        })
        return newMap
      })
    }, options)

    return () => observerRef.current?.disconnect()
  }, [options])

  const observe = useCallback((element, id) => {
    if (!element || !observerRef.current) return

    element.setAttribute('data-observe-id', id)
    observerRef.current.observe(element)
  }, [])

  const isVisible = useCallback(
    (id) => visibilityMap.get(id) ?? false,
    [visibilityMap]
  )

  return { observe, isVisible, visibilityMap }
}

API Reference

useIntersectionObserver

Parameters

options?: IntersectionObserverInit

Optional configuration (threshold, root, rootMargin)

Returns

[ref, isVisible] - Ref to attach to element and boolean visibility state

useIntersectionObserverMultiple

Parameters

options?: IntersectionObserverInit

Optional configuration (threshold, root, rootMargin)

Returns

observe(element, id) - Function to observe an element with a unique ID

isVisible(id) - Function to check if element with ID is visible

visibilityMap - Map of all element IDs and their visibility states

💡 Use Cases

  • Lazy loading images when they enter viewport
  • Triggering animations on scroll
  • Infinite scroll pagination
  • Analytics tracking for element visibility