TIL: avoid scrolling events with IntersectionObserver API

Today at work I had to implement a header component that was supposed to shrink into a more compact version of itself once the user started scrolling down the page.

I immediately dreaded it as the only solution I could think of would be to listen to the scroll event and face rerenderings and possible performance issues until my colleague suggested I could use the Intersection Observer API.

Here’s a simplified version of how I implemented a React component with a behaviour that depends on scrolling without an event listener:

function HeaderComponent() {
	const [isCompact, setIsCompact] = useState(false)

	/** This is used to detect whether the page content has scrolled
	 * outside of the viewport and determine if the header should
	 * collapse without listening on page scroll events
	 */
	const targetRef = useCallback((node) => {
		const intersectionObserver = new IntersectionObserver((entries) => {
			const isElementOffViewport = !entries[0].isIntersecting

			setIsCompact(isElementOffViewport)
		})

		intersectionObserver.observe(node)
	}, [])

	return (
		<>
			<header style={{ position: 'fixed' }}>
				{/* Here goes the header code, using `isCompact` to determine how to render things in each state */}
			</header>

			{/* This is an invisible element that will be used to determine whether the content has scrolled. Because the header's position is `fixed`, this div will scroll behind it and as soon as it leaves the viewport (isIntersecting changes from true to false), the observer will call `setIsCompact` */}
			<div ref={targetRef} />
		</>
	)
}

The IntersectionObserver will trigger the callback whenever there’s a change in whether the target element intersects with another element (or with the document’s viewport if not specified).

Because I needed the header to be sticky at the top, I added an invisible div as the target element. Instead of listening to every scroll event, we only update the state once this div reaches the top of the page!



In this implementation, I also chose to use a ref callback instead of the usual useRef + useEffect combo, after reading this article by @tkdodo. I’m trying to be more conscious of my usage of useEffect since then and it actually helped me to understand what the code is doing, without unnecessary effects.

Written on a Wednesday in September, 2022