Skip to content

use-intersection-observer

A React hook that provides a declarative way to observe element visibility using the Intersection Observer API.

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

  • Auto cleanup: Automatic cleanup of observers on unmount and dependency change
  • Reactive: Potentially re-attaches observers on dependency change(element, onlyTriggerOnce)
  • Flexible keys: Support for custom property naming through the key parameter with full-type safety
  • One-time observation: Built-in support for observing elements only once
  • Standard options: Full support for all IntersectionObserverInit options (root, rootMargin, threshold)

TIP

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

Problem It Solves

Multiple Instance Management with Type-Safety
  • Type-Safe & Dynamic Key Property Generation: Each instance gets uniquely named properties with full TypeScript support and IntelliSense
  • Scalable Architecture: Can observe unlimited elements without property collisions
tsx
// Without key
const { element, setElementRef, isElementIntersecting } = useIntersectionObserver()

// With key 'sidebar'
const { sidebarElement, setSidebarElementRef, isSidebarElementIntersecting } = useIntersectionObserver({
   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>
   )
}