The above answers helped me go a long way. However, none of the solutions seems to avoid creating multiple IntersectionObserver
objects. So I present the following solution which seems to me to avoid said shortcoming. Any feedback is highly appreciated if you see any shortcomings, pitfalls, or smells.
Hook
@hooks/useIntersectionObserver.tsx
import {
PropsWithChildren,
RefObject,
createContext,
useContext,
useEffect,
useRef,
useState,
} from "react";
export const INTERSECTION_OBSERVER_ROOT_ID =
"your-app-name-plus-something-to-create-a-unique-id";
// TO BE USED ONLY ONCE (wrapping the INTERSECTION_OBSERVER_ROOT element)
// OTHERWISE IT WILL CREATE MULTIPLE OBSERVERS
const IntersectionObserverContext = createContext<{
ref: RefObject<IntersectionObserver | null>;
intersectingEntries: IntersectionObserverEntry[];
}>({
ref: { current: null },
intersectingEntries: [],
});
export const IntersectionObserverProvider = ({
children,
}: PropsWithChildren) => {
const interserctionObserverRef = useRef<IntersectionObserver | null>(null);
const [intersectingEntries, setIntersectingEntries] = useState<
IntersectionObserverEntry[]
>([]);
useEffect(() => {
const intersectionRoot = document.getElementById(
INTERSECTION_OBSERVER_ROOT_ID,
);
const interRootBoundingClientRect =
intersectionRoot?.getBoundingClientRect();
// Calculating a rootMargin as it was required for my use case
const interRootHeight = interRootBoundingClientRect?.height ?? 0;
const rootMarginBottom = -interRootHeight;
const rootMargin = `0px 0px ${rootMarginBottom}px 0px`;
interserctionObserverRef.current = new IntersectionObserver(
(entries) =>
setIntersectingEntries(entries.filter((entry) => entry.isIntersecting)),
{
root: intersectionRoot,
rootMargin: rootMargin,
},
);
}, []);
return (
<IntersectionObserverContext.Provider
value={{
ref: interserctionObserverRef,
intersectingEntries: intersectingEntries,
}}
>
{children}
</IntersectionObserverContext.Provider>
);
};
// TO BE USED ONLY ONCE (wrapping the INTERSECTION_OBSERVER_ROOT element)
export default function useIntersectionObserver(
ref: RefObject<HTMLElement>,
) {
const { ref: interserctionObserverRef, intersectingEntries } = useContext(
IntersectionObserverContext,
);
const [isIntersecting, setIsIntersecting] = useState(false);
useEffect(() => {
const currIObserverRef = interserctionObserverRef.current;
if (ref.current && currIObserverRef) {
currIObserverRef.observe(ref.current as HTMLElement);
}
return () => {
currIObserverRef?.disconnect();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ref]);
useEffect(() => {
if (intersectingEntries.length > 0) {
const entryTarget = intersectingEntries.find(
(entry) => entry.target === ref.current,
);
if (entryTarget) {
setIsIntersecting(true);
return;
}
setIsIntersecting(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [intersectingEntries]);
return isIntersecting;
}
Usage
Component.tsx
import { PropsWithChildren } from "react";
import {
INTERSECTION_OBSERVER_ROOT_ID,
IntersectionObserverProvider,
} from "@/hooks/useIntersectionObserver";
type ComponentProps = {};
const Component = ({children}: PropsWithChildren<ComponentProps>) => {
return (
<div>
<IntersectionObserverProvider>
<div id={INTERSECTION_OBSERVER_ROOT_ID}>
{/* You could have as many ChildComponents that use the hook as you want*/}
{children}
</div>
</IntersectionObserverProvider>
</div>
);
};
export default Component;
ChildComponent.tsx
import { PropsWithChildren, useRef, useState } from "react";
import useAPIDocIntersectionObserver from "@/hooks/useIntersectionObserver";
type ChildComponentProps = {
id: string;
title: string;
};
const ChildComponent = ({
id,
title,
children,
}: PropsWithChildren<ChildComponentProps>) => {
const ref = useRef<HTMLDivElement>(null);
const isIntersecting = useAPIDocIntersectionObserver(ref);
if (isIntersecting) {
console.log(id, "Intersecting");
}
return (
<div ref={ref} id={id}>
<h1 href={`#${id}`} className={styles.link}>
{title}
</h1>
<div>
{children}
</div>
</div>
);
};
export default ChildComponent;
If you find any syntactical errors, please do comment below. Cheers!