useIntersectionObserver()

This React Hook detects visibility of a component on the viewport using the IntersectionObserver API natively present in the browser.

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

Your must pass the ref element (from useRef()).

It takes optionally root, rootMargin and threshold arguments from the native IntersectionObserver API and freezeOnceVisible to only catch the first appearance too.

It returns the full IntersectionObserver's entry object.


Source:

I discovered this way of using IntersectionObserver via this post medium while playing to build a lazy-loaded collection of images.

The Hook

1import { RefObject, useEffect, useState } from 'react'
2
3interface Args extends IntersectionObserverInit {
4 freezeOnceVisible?: boolean
5}
6
7function useIntersectionObserver(
8 elementRef: RefObject<Element>,
9 {
10 threshold = 0,
11 root = null,
12 rootMargin = '0%',
13 freezeOnceVisible = false,
14 }: Args,
15): IntersectionObserverEntry | undefined {
16 const [entry, setEntry] = useState<IntersectionObserverEntry>()
17
18 const frozen = entry?.isIntersecting && freezeOnceVisible
19
20 const updateEntry = ([entry]: IntersectionObserverEntry[]): void => {
21 setEntry(entry)
22 }
23
24 useEffect(() => {
25 const node = elementRef?.current // DOM Ref
26 const hasIOSupport = !!window.IntersectionObserver
27
28 if (!hasIOSupport || frozen || !node) return
29
30 const observerParams = { threshold, root, rootMargin }
31 const observer = new IntersectionObserver(updateEntry, observerParams)
32
33 observer.observe(node)
34
35 return () => observer.disconnect()
36
37 // eslint-disable-next-line react-hooks/exhaustive-deps
38 }, [elementRef?.current, JSON.stringify(threshold), root, rootMargin, frozen])
39
40 return entry
41}
42
43export default useIntersectionObserver

Usage

1import { useRef } from 'react'
2
3import { useIntersectionObserver } from 'usehooks-ts'
4
5const Section = (props: { title: string }) => {
6 const ref = useRef<HTMLDivElement | null>(null)
7 const entry = useIntersectionObserver(ref, {})
8 const isVisible = !!entry?.isIntersecting
9
10 console.log(`Render Section ${props.title}`, { isVisible })
11
12 return (
13 <div
14 ref={ref}
15 style={{
16 minHeight: '100vh',
17 display: 'flex',
18 border: '1px dashed #000',
19 fontSize: '2rem',
20 }}
21 >
22 <div style={{ margin: 'auto' }}>{props.title}</div>
23 </div>
24 )
25}
26
27export default function Component() {
28 return (
29 <>
30 {Array.from({ length: 5 }).map((_, index) => (
31 <Section key={index + 1} title={`${index + 1}`} />
32 ))}
33 </>
34 )
35}

Edit on CodeSandbox

See a way to make this page better?
Edit there »