INTERMEDIATEREACTUI/UX
useIntersectionObserver Hook
A performant React hook to track when an element enters or leaves the viewport. Perfect for scroll animations and lazy loading.
Published Dec 2, 2025
reacthooksanimationperformance
useIntersectionObserver Hook
This hook wraps the native IntersectionObserver API to provide a declarative way to track element visibility. It's essential for implementing "fade-in on scroll" effects, lazy loading images, or infinite scrolling lists.
Highlights
- Performance First: Uses a single observer instance (per unique options) or cleans up efficiently.
- Freeze Once Visible: Optional
freezeOnceVisibleflag to stop observing after the element enters the viewport (great for one-off animations). - Type Safe: Fully typed with TypeScript.
Code
import { useEffect, useRef, useState } from 'react';
interface UseIntersectionObserverArgs extends IntersectionObserverInit {
freezeOnceVisible?: boolean;
}
export function useIntersectionObserver(
elementRef: React.RefObject<Element | null>,
{
threshold = 0,
root = null,
rootMargin = '0%',
freezeOnceVisible = false,
}: UseIntersectionObserverArgs = {}
): IntersectionObserverEntry | undefined {
const [entry, setEntry] = useState<IntersectionObserverEntry>();
const frozen = entry?.isIntersecting && freezeOnceVisible;
useEffect(() => {
const node = elementRef?.current;
const hasIOSupport = !!window.IntersectionObserver;
if (!hasIOSupport || frozen || !node) return;
const observerParams = { threshold, root, rootMargin };
const observer = new IntersectionObserver(([newEntry]) => {
setEntry(newEntry);
}, observerParams);
observer.observe(node);
return () => observer.disconnect();
}, [elementRef, JSON.stringify(threshold), root, rootMargin, frozen]);
return entry;
}Usage
Here is how you can use it to create a "Fade In" animation component:
import { useRef } from 'react';
import { useIntersectionObserver } from '@/hooks/useIntersectionObserver';
export function FadeInSection({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
const entry = useIntersectionObserver(ref, {
freezeOnceVisible: true, // Only fade in once
threshold: 0.1, // Trigger when 10% visible
});
const isVisible = !!entry?.isIntersecting;
return (
<div
ref={ref}
className={`transition-opacity duration-1000 ${
isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'
}`}
>
{children}
</div>
);
}๐ก Pro Tip: For complex lists with hundreds of items, consider using a single parent observer instead of creating a hook for every item to save memory.