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.
Usage
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}`} />
))}
</>
)
}
Hook
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
}
Parameters
Name | Type | Default | Description |
---|---|---|---|
root | Element | Document | null | null | The root element to use for intersection checking. Defaults to the browser viewport. |
rootMargin | string | '0%' | Margin around the root element. Values are similar to CSS margin property (e.g., '0% 0% 0% 0%' — top%, right%, bottom%, left%). |
threshold | number | number[] | 0 | A 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. |
freezeOnceVisible | boolean | false | If true , stops observing once the element becomes visible. |
onChange | (isIntersecting: boolean, entry: IntersectionObserverEntry) => void | undefined | Optional callback fired when the element’s intersection state changes. |
initialIsIntersecting | boolean | false | Initial value for isIntersecting before the first intersection check. |
Returns
Name | Type | Description |
---|---|---|
ref | (node?: Element | null) => void | Callback ref to assign to the element you want to observe. |
isIntersecting | boolean | Indicates whether the observed element is currently intersecting the viewport (or root). |
entry | IntersectionObserverEntry | undefined | The 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)