useIntersectionObserver

All about useIntersectionObserver hook in React


A custom React hook to observe the visibility of an element within the viewport using the Intersection Observer API.

LinkIconUsage

ReactIcon
import { useIntersectionObserver } from 'usehooks-ts'
 
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}`} />
      ))}
    </>
  )
}

LinkIconHook

TypescriptIcon
import { useEffect, useRef, useState } from 'react'
 
type State = {
  isIntersecting: boolean
  entry?: IntersectionObserverEntry
}
 
type UseIntersectionObserverOptions = {
  root?: Element | Document | null
  rootMargin?: string
  threshold?: number | number[]
  freezeOnceVisible?: boolean
  onChange?: (isIntersecting: boolean, entry: IntersectionObserverEntry) => void
  initialIsIntersecting?: boolean
}
 
type IntersectionReturn = [
  (node?: Element | null) => void,
  boolean,
  IntersectionObserverEntry | undefined,
] & {
  ref: (node?: Element | null) => void
  isIntersecting: boolean
  entry?: IntersectionObserverEntry
}
 
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<State>(() => ({
    isIntersecting: initialIsIntersecting,
    entry: undefined,
  }))
 
  const callbackRef = useRef<UseIntersectionObserverOptions['onChange']>()
 
  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
}

LinkIconParameters

NameTypeDefaultDescription
rootElement | Document | nullnullThe root element to use for intersection checking. Defaults to the browser viewport.
rootMarginstring'0%'Margin around the root element. Values are similar to CSS margin property (e.g., '0% 0% 0% 0%' — top%, right%, bottom%, left%).
thresholdnumber | number[]0A single number or array of numbers between 0 and 1, indicating at what percentage(s) of the target's visibility the observer’s callback should execute.
freezeOnceVisiblebooleanfalseIf true, stops observing once the element becomes visible.
onChange(isIntersecting: boolean, entry: IntersectionObserverEntry) => voidundefinedOptional callback fired when the element’s intersection state changes.
initialIsIntersectingbooleanfalseInitial value for isIntersecting before the first intersection check.

LinkIconReturns

NameTypeDescription
ref(node?: Element | null) => voidCallback ref to assign to the element you want to observe.
isIntersectingbooleanIndicates whether the observed element is currently intersecting the viewport (or root).
entryIntersectionObserverEntry | undefinedThe latest IntersectionObserverEntry for the observed element.

The hook also supports array destructuring:

const [ref, isIntersecting, entry] = useIntersectionObserver(options)

Or object destructuring:

const { ref, isIntersecting, entry } = useIntersectionObserver(options)