Skip to main content

All hooks

useIntersectionObserver

Custom hook that tracks the intersection of a DOM element with its containing element or the viewport using the [`Intersection Observer API`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).


It can be very useful to lazy-loading of images, implementing "infinite scrolling", tracking view in GA or starting animations for example.

Option properties

  • threshold (optional, default: 0): A threshold indicating the percentage of the target's visibility needed to trigger the callback. Can be a single number or an array of numbers.
  • root (optional, default: null): The element that is used as the viewport for checking visibility of the target. It can be an Element, Document, or null.
  • rootMargin (optional, default: '0%'): A margin around the root. It specifies the size of the root's margin area.
  • freezeOnceVisible (optional, default: false): If true, freezes the intersection state once the element becomes visible. Once the element enters the viewport and triggers the callback, further changes in intersection will not update the state.
  • onChange (optional): A callback function to be invoked when the intersection state changes. It receives two parameters: isIntersecting (a boolean indicating if the element is intersecting) and entry (an IntersectionObserverEntry object representing the state of the intersection).
  • initialIsIntersecting (optional, default: false): The initial state of the intersection. If set to true, indicates that the element is intersecting initially.

Note: This interface extends the native IntersectionObserverInit interface, which provides the base options for configuring the Intersection Observer.

For more information on the Intersection Observer API and its options, refer to the MDN Intersection Observer API documentation.

Return

The IntersectionResult type supports both array and object destructuring and includes the following properties:

  • ref: A function that can be used as a ref callback to set the target element.
  • isIntersecting: A boolean indicating if the target element is intersecting with the viewport.
  • entry: An optional IntersectionObserverEntry object representing the state of the intersection.

Usage

import { useIntersectionObserver } from './useIntersectionObserver'

const Section = (props: { title: string }) => {
  const { isIntersecting, ref } = useIntersectionObserver({
    threshold: 0.5,
  })

  console.log(`Render Section ${props.title}`, {
    isIntersecting,
  })

  return (
    <div
      ref={ref}
      style={{
        minHeight: '100vh',
        display: 'flex',
        border: '1px dashed #000',
        fontSize: '2rem',
      }}
    >
      <div style={{ margin: 'auto' }}>{props.title}</div>
    </div>
  )
}

export default function Component() {
  return (
    <>
      {Array.from({ length: 5 }).map((_, index) => (
        <Section key={index + 1} title={`${index + 1}`} />
      ))}
    </>
  )
}

API

function useIntersectionObserver(options: UseIntersectionObserverOptions): IntersectionReturn

Custom hook that tracks the intersection of a DOM element with its containing element or the viewport using the Intersection Observer API.

Parameters

NameTypeDefault valueDescription
optionsUseIntersectionObserverOptions{}The options for the Intersection Observer.

Returns

The ref callback, a boolean indicating if the element is intersecting, and the intersection observer entry.

Type declaration

IntersectionReturn

The return type of the useIntersectionObserver hook. Supports both tuple and object destructing.

UseIntersectionObserverOptions

Represents the options for configuring the Intersection Observer.

NameTypeDescription
freezeOnceVisiblebooleanIf true, freezes the intersection state once the element becomes visible.
initialIsIntersectingbooleanThe initial state of the intersection.
onChange(isIntersecting: boolean, entry: IntersectionObserverEntry) => voidA callback function to be invoked when the intersection state changes.
root`ElementDocument
rootMarginstringA margin around the root.
threshold`numbernumber[]`

UseIntersectionObserverState

The hook internal state.

NameTypeDescription
entryIntersectionObserverEntryThe intersection observer entry.
isIntersectingbooleanA boolean indicating if the element is intersecting.

Hook

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

/** The hook internal state. */
export type UseIntersectionObserverState = {
  /** A boolean indicating if the element is intersecting. */
  isIntersecting: boolean
  /** The intersection observer entry. */
  entry?: IntersectionObserverEntry
}

/** Represents the options for configuring the Intersection Observer. */
export type UseIntersectionObserverOptions = {
  /**
   * The element that is used as the viewport for checking visibility of the target.
   * @default null
   */
  root?: Element | Document | null
  /**
   * A margin around the root.
   * @default '0%'
   */
  rootMargin?: string
  /**
   * A threshold indicating the percentage of the target's visibility needed to trigger the callback.
   * @default 0
   */
  threshold?: number | number[]
  /**
   * If true, freezes the intersection state once the element becomes visible.
   * @default false
   */
  freezeOnceVisible?: boolean
  /**
   * A callback function to be invoked when the intersection state changes.
   * @param {boolean} isIntersecting - A boolean indicating if the element is intersecting.
   * @param {IntersectionObserverEntry} entry - The intersection observer Entry.
   * @default undefined
   */
  onChange?: (isIntersecting: boolean, entry: IntersectionObserverEntry) => void
  /**
   * The initial state of the intersection.
   * @default false
   */
  initialIsIntersecting?: boolean
}

/**
 * The return type of the useIntersectionObserver hook.
 *
 * Supports both tuple and object destructing.
 * @param {(node: Element | null) => void} ref - The ref callback function.
 * @param {boolean} isIntersecting - A boolean indicating if the element is intersecting.
 * @param {IntersectionObserverEntry | undefined} entry - The intersection observer Entry.
 */
export type IntersectionReturn = [
  (node?: Element | null) => void,
  boolean,
  IntersectionObserverEntry | undefined,
] & {
  ref: (node?: Element | null) => void
  isIntersecting: boolean
  entry?: IntersectionObserverEntry
}

/**
 * Custom hook that tracks the intersection of a DOM element with its containing element or the viewport using the [`Intersection Observer API`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
 * @param {UseIntersectionObserverOptions} options - The options for the Intersection Observer.
 * @returns {IntersectionReturn} The ref callback, a boolean indicating if the element is intersecting, and the intersection observer entry.
 * @public
 * @see [Documentation](https://usehooks-ts.com/react-hook/use-intersection-observer)
 * @example
 * ```tsx
 * // Example 1
 * const [ref, isIntersecting, entry] = useIntersectionObserver({ threshold: 0.5 });
 * ```
 *
 * ```tsx
 * // Example 2
 * const { ref, isIntersecting, entry } = useIntersectionObserver({ threshold: 0.5 });
 * ```
 */
export function useIntersectionObserver({
  threshold = 0,
  root = null,
  rootMargin = '0%',
  freezeOnceVisible = false,
  initialIsIntersecting = false,
  onChange,
}: UseIntersectionObserverOptions = {}): IntersectionReturn {
  const [ref, setRef] = useState<Element | null>(null)

  const [state, setState] = useState<UseIntersectionObserverState>(() => ({
    isIntersecting: initialIsIntersecting,
    entry: undefined,
  }))

  const callbackRef = useRef<UseIntersectionObserverOptions['onChange']>(undefined)

  callbackRef.current = onChange

  const frozen = state.entry?.isIntersecting && freezeOnceVisible

  useEffect(() => {
    // Ensure we have a ref to observe
    if (!ref) return

    // Ensure the browser supports the Intersection Observer API
    if (!('IntersectionObserver' in window)) return

    // Skip if frozen
    if (frozen) return

    let unobserve: (() => void) | undefined

    const observer = new IntersectionObserver(
      (entries: IntersectionObserverEntry[]): void => {
        const thresholds = Array.isArray(observer.thresholds)
          ? observer.thresholds
          : [observer.thresholds]

        entries.forEach(entry => {
          const isIntersecting =
            entry.isIntersecting &&
            thresholds.some(threshold => entry.intersectionRatio >= threshold)

          setState({ isIntersecting, entry })

          if (callbackRef.current) {
            callbackRef.current(isIntersecting, entry)
          }

          if (isIntersecting && freezeOnceVisible && unobserve) {
            unobserve()
            unobserve = undefined
          }
        })
      },
      { threshold, root, rootMargin },
    )

    observer.observe(ref)

    return () => {
      observer.disconnect()
    }

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [
    ref,
    // eslint-disable-next-line react-hooks/exhaustive-deps
    JSON.stringify(threshold),
    root,
    rootMargin,
    frozen,
    freezeOnceVisible,
  ])

  // ensures that if the observed element changes, the intersection observer is reinitialized
  const prevRef = useRef<Element | null>(null)

  useEffect(() => {
    if (
      !ref &&
      state.entry?.target &&
      !freezeOnceVisible &&
      !frozen &&
      prevRef.current !== state.entry.target
    ) {
      prevRef.current = state.entry.target
      setState({ isIntersecting: initialIsIntersecting, entry: undefined })
    }
  }, [ref, state.entry, freezeOnceVisible, frozen, initialIsIntersecting])

  const result = [
    setRef,
    !!state.isIntersecting,
    state.entry,
  ] as IntersectionReturn

  // Support object destructuring, by adding the specific values.
  result.ref = result[0]
  result.isIntersecting = result[1]
  result.entry = result[2]

  return result
}