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) andentry(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 optionalIntersectionObserverEntryobject 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
| Name | Type | Default value | Description |
|---|---|---|---|
options | UseIntersectionObserverOptions | {} | 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.
| Name | Type | Description |
|---|---|---|
freezeOnceVisible | boolean | If true, freezes the intersection state once the element becomes visible. |
initialIsIntersecting | boolean | The initial state of the intersection. |
onChange | (isIntersecting: boolean, entry: IntersectionObserverEntry) => void | A callback function to be invoked when the intersection state changes. |
root | `Element | Document |
rootMargin | string | A margin around the root. |
threshold | `number | number[]` |
UseIntersectionObserverState
The hook internal state.
| Name | Type | Description |
|---|---|---|
entry | IntersectionObserverEntry | The intersection observer entry. |
isIntersecting | boolean | A 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
}