typescriptintermediate
React Intersection Observer Hook
Custom useIntersectionObserver hook for lazy loading, infinite scroll, and scroll animations.
typescriptPress ⌘/Ctrl + Shift + C to copy
'use client';
import { useEffect, useRef, useState, useCallback } from 'react';
interface UseIntersectionOptions {
threshold?: number;
rootMargin?: string;
triggerOnce?: boolean;
}
export function useIntersectionObserver({
threshold = 0,
rootMargin = '0px',
triggerOnce = false,
}: UseIntersectionOptions = {}) {
const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
const [node, setNode] = useState<Element | null>(null);
const ref = useCallback((el: Element | null) => setNode(el), []);
useEffect(() => {
if (!node) return;
const observer = new IntersectionObserver(
([entry]) => {
setEntry(entry);
if (triggerOnce && entry.isIntersecting) {
observer.unobserve(node);
}
},
{ threshold, rootMargin }
);
observer.observe(node);
return () => observer.disconnect();
}, [node, threshold, rootMargin, triggerOnce]);
return { ref, entry, isIntersecting: entry?.isIntersecting ?? false };
}
// Lazy load component
function LazySection({ children }: { children: React.ReactNode }) {
const { ref, isIntersecting } = useIntersectionObserver({
triggerOnce: true,
rootMargin: '200px',
});
return (
<div ref={ref}>
{isIntersecting ? children : <div className="h-64 animate-pulse bg-gray-800 rounded" />}
</div>
);
}
// Infinite scroll
function InfiniteList() {
const [items, setItems] = useState<string[]>(Array.from({ length: 20 }, (_, i) => `Item ${i}`));
const { ref, isIntersecting } = useIntersectionObserver({ rootMargin: '100px' });
useEffect(() => {
if (isIntersecting) {
setItems((prev) => [
...prev,
...Array.from({ length: 10 }, (_, i) => `Item ${prev.length + i}`),
]);
}
}, [isIntersecting]);
return (
<div>
{items.map((item) => <div key={item} className="p-4 border-b">{item}</div>)}
<div ref={ref} className="h-4" />
</div>
);
}
// Scroll animation
function AnimatedCard({ children }: { children: React.ReactNode }) {
const { ref, isIntersecting } = useIntersectionObserver({ threshold: 0.2, triggerOnce: true });
return (
<div ref={ref} className={`transition-all duration-700 ${
isIntersecting ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'
}`}>{children}</div>
);
}Use Cases
- Lazy loading components when they enter viewport
- Infinite scroll pagination
- Scroll-triggered animations and transitions
Tags
Related Snippets
Similar patterns you can reuse in the same workflow.
typescriptintermediate
Infinite Scroll with Intersection Observer
Load more items as the user scrolls using IntersectionObserver. No external libraries required.
Best for: Feed pagination
#infinite-scroll#intersection-observer
typescriptintermediate
useThrottle Hook for Rate Limiting
Throttle rapidly-firing values like scroll or resize events with a configurable delay hook.
Best for: Scroll position tracking
#react#hooks
typescriptbeginner
useDebounce — Debounced Value Hook
Debounce any rapidly-changing value with a configurable delay. Useful for search inputs and resize handlers.
Best for: Search-as-you-type
#hooks#debounce
typescriptintermediate
useIntersectionObserver Hook
Track element visibility with the Intersection Observer API for lazy loading and scroll animations.
Best for: Lazy loading images
#hooks#intersection-observer