useIntersection Hook
A custom React hook to track visibility of multiple items using the Intersection Observer API.
import { useState, useEffect } from "react";
import { useIntersection } from "./useIntersection";
const items = [
{ id: 1 },
{ id: 2 },
{ id: 3 }
];
export default function App() {
const { visibleIds } = useIntersection(items, { threshold: 0.3, prefix: "item" });
return (
<div>
{items.map((item) => (
<div
key={item.id}
id={`item-${item.id}`}
style={{ height: "100px", margin: "20px 0", background: visibleIds.has(item.id) ? "lightgreen" : "lightgray" }}
>
Item {item.id}
</div>
))}
</div>
);
}
Hook Implementation
"use client";
import { useState, useEffect } from "react";
interface UseIntersectionOptions {
threshold?: number;
prefix?: string;
}
export function useIntersection(
items: { id: string | number }[],
options: UseIntersectionOptions = {}
) {
const [visibleIds, setVisibleIds] = useState<Set<string | number>>(
new Set()
);
useEffect(() => {
const handleIntersection = (entries: IntersectionObserverEntry[]) => {
setVisibleIds((prev) => {
const next = new Set(prev);
for (const entry of entries) {
const id = entry.target.id.replace(
`${options.prefix}-`,
""
);
const parsedId = Number.isNaN(Number(id)) ? id : Number(id);
if (entry.isIntersecting) {
next.add(parsedId);
} else {
next.delete(parsedId);
}
}
return next;
});
};
const observer = new IntersectionObserver(handleIntersection, {
threshold: options.threshold || 0.2,
rootMargin: "-50px 0px",
});
for (const item of items) {
const element = document.getElementById(
`${options.prefix}-${item.id}`
);
if (element) observer.observe(element);
}
return () => observer.disconnect();
}, [items, options.prefix, options.threshold]);
return { visibleIds };
}