Skip to content

use-intersection-observer

use-intersection-observer is a declarative React hook that simplifies observing element visibility using the native Intersection Observer API.

It abstracts observer creation, lifecycle management, and cleanup while remaining fully type-safe. The hook supports dynamic property naming, one-time observation, and all standard observer options. It also gracefully handles unsupported environments by avoiding observer creation and warning in development.

Important

This hook automatically checks for IntersectionObserver support and logs a warning in development if it's not available. The hook will gracefully handle unsupported browsers by not creating observers.

Features

  • Declarative API: Observe element visibility without manual observer management
  • Automatic cleanup: Observers disconnect on unmount or dependency changes
  • Reactive re-attachment: Updates automatically when the element or options change
  • One-time observation: onlyTriggerOnce support for single-intersection cases
  • Dynamic property keys: Custom key for type-safe, IntelliSense-friendly state
  • Full options support: Supports all IntersectionObserverInit options (root, rootMargin, threshold)
  • Intersection callbacks: Optional onIntersection for fine-grained control
  • Optimized reactivity: Avoids unnecessary re-renders via use-synced-ref
  • Graceful degradation: Safely handles environments without IntersectionObserver

TIP

This hook internally uses useSyncedRef to avoid unnecessary re-renders when callback functions change

Problem It Solves

Eliminate manual setup
  • Eliminates manual setup and cleanup of IntersectionObserver instances
  • Prevents memory leaks caused by orphaned observers
  • Removes complexity when observing elements only once
  • Avoids brittle state handling for intersection status
Type-Safe and Collision free Instance Management
  • Dynamic & Type-Safe Properties: Generates uniquely named properties for each observer with full TypeScript and IntelliSense support.
  • Scalable: Efficiently observe any number of elements without property naming conflicts.
tsx
// Example: Dynamically named properties based on the 'key'
const { element, setElementRef, isElementIntersecting } = useIntersectionObserver() // Without key
const { sidebarElement, setSidebarElementRef, isSidebarElementIntersecting } = useIntersectionObserver({
   key: 'sidebar', // With key 'sidebar'
})
One-Time Observation Complexity
  • onlyTriggerOnce option for automatically cleaning up observer after first intersection

Parameters

ParameterTypeRequiredDefault ValueDescription
optionsIntersectionObserverOptionsundefinedConfiguration object for the intersection observer

Options Parameter

The options parameter accepts an object that extends the standard IntersectionObserverInit with an additional custom property for conditional event handling and post callback.

Standard IntersectionObserverInit Options

PropertyTypeDefaultDescription
keystring''Custom key for property naming
rootElement | Document | nullnullRoot element for intersection
rootMarginstring0pxMargin around root element
thresholdnumber | number[]0Intersection ratio threshold(s)

Custom Options

PropertyTypeDefaultDescription
onlyTriggerOncebooleanfalseControls whether to observe only the initial intersection
onIntersection(entry: IntersectionObserverEntry) => voidundefinedCallback fired on every intersection of the element

Type Definitions

Details
ts
export interface BaseIntersectionObserverOptions {
   onIntersection?: (entry: IntersectionObserverEntry) => void
   onlyTriggerOnce?: boolean
}

export interface IntersectionObserverOptions<Key extends string = ''>
   extends IntersectionObserverInit,
      BaseIntersectionObserverOptions {
   key?: Key
}

export type IntersectionObserverResult<Key extends string> = {
   // Dynamic property names based on key
   [K in Key as Key extends '' ? 'element' : `${Key}Element`]: HTMLElement | null
} & {
   [K in Key as Key extends '' ? 'setElementRef' : `set${Capitalize<Key>}ElementRef`]: (
      elementNode: HTMLElement | null
   ) => void
} & {
   [K in Key as Key extends '' ? 'isElementIntersecting' : `is${Capitalize<Key>}ElementIntersecting`]: boolean
}

Return Value(s)

The hook returns an object with dynamically named properties based on the key parameter:

  • Without key: element, setElementRef, isElementIntersecting
  • With key: {key}Element, set{Key}ElementRef, is{Key}ElementIntersecting

INFO

element: Holds the reference of element which is being observed, initial value is undefined.

setElementRef: A Setter function to store the element reference within element, which will be observed.

isElementIntersecting: Holds the boolean intersection status of the element weather it is intersecting the screen or not.

Common Use Cases

  • Lazy loading: Load images or content when they come into view
  • Infinite scrolling: Load more content when reaching the end
  • Performance optimization: Pause expensive operations when elements are not visible

Usage Examples

Basic Intersection Observer

tsx
import { useIntersectionObserver } from 'classic-react-hooks'

export default function BasicExample() {
   const { element, setElementRef, isElementIntersecting } = useIntersectionObserver({
      threshold: 0.5,
      onIntersection: (entry) => {
         console.log('Intersection changed:', entry.isIntersecting)
      },
   })

   return (
      <div className='h-[200vh]'>
         <div className='mt-[100vh]'>
            <div ref={setElementRef} className={`p-5 ${isElementIntersecting ? 'bg-green-200' : 'bg-red-200'}`}>
               {isElementIntersecting ? 'Visible!' : 'Not visible'}
            </div>
         </div>
      </div>
   )
}

Using Custom Keys

Example
tsx
import { useIntersectionObserver } from 'classic-react-hooks'

export default function CustomKeyExample() {
   const { heroElement, setHeroElementRef, isHeroElementIntersecting } = useIntersectionObserver({
      key: 'hero', 
      threshold: 0.3,
      rootMargin: '-50px',
   })

   return (
      <div>
         <header
            ref={setHeroElementRef}
            className={`h-96 ${
               isHeroElementIntersecting ? 'bg-blue-500' : 'bg-gray-500'
            } text-white flex items-center justify-center`}
         >
            <h1 className='text-3xl font-bold'>Hero Section {isHeroElementIntersecting ? '(Visible)' : '(Hidden)'}</h1>
         </header>
         <div className='h-[200vh] p-5'>
            <p>Scroll to see the hero section intersection status change</p>
         </div>
      </div>
   )
}

One-Time Trigger

Example
tsx
import { useState } from 'react'
import { useIntersectionObserver } from 'classic-react-hooks'

export default function OneTimeExample() {
   const [hasBeenSeen, setHasBeenSeen] = useState(false)

   const { setElementRef, isElementIntersecting } = useIntersectionObserver({
      onlyTriggerOnce: true, 
      threshold: 0.8,
      onIntersection: (entry) => {
         if (entry.isIntersecting) {
            setHasBeenSeen(true)
            console.log('Element seen for the first time!')
         }
      },
   })

   return (
      <div className='h-[200vh]'>
         <div className='mt-[150vh]'>
            <div ref={setElementRef} className={`p-10 text-center ${hasBeenSeen ? 'bg-yellow-300' : 'bg-blue-200'}`}>
               {hasBeenSeen ? 'I was seen!' : 'Scroll down to see me'}
            </div>
         </div>
      </div>
   )
}

Multiple Thresholds

Example
tsx
import { useIntersectionObserver } from 'classic-react-hooks'

export default function MultipleThresholdsExample() {
   const { setElementRef, isElementIntersecting } = useIntersectionObserver({
      threshold: [0, 0.25, 0.5, 0.75, 1.0], 
      onIntersection: (entry) => {
         const percentage = Math.round(entry.intersectionRatio * 100)
         console.log(`Element is ${percentage}% visible`)
      },
   })

   return (
      <div className='h-[200vh] p-5'>
         <div className='h-screen' />
         <div
            ref={setElementRef}
            className={`h-72 ${isElementIntersecting ? 'bg-green-200' : 'bg-red-200'} flex items-center justify-center`}
         >
            <h2 className='text-xl font-semibold'>Check console for intersection percentage</h2>
         </div>
      </div>
   )
}