It was interesting enough.
To achieve your goal, I have created a HoC, a wrapper. This greatly simplifies the task and will reduce the likelihood of errors with the react lifecycle and refs management.
And just a note - in case child items will update their state and rerender - they will be removed from Map that was used due to HoC callback function and Map that is using "them", the "children"s as a key.
Not sure about memory leaks, didnt notice any in final result (and had a lot Out of Memory during development)
wrapper component:
import React, { useMemo, useCallback, useEffect, createRef } from "react";
const IntersectionObserverWrapper = (props) => {
const { children, onVisibilityChanged, onElementDestroyed } = props;
const wrapperRef = createRef();
const options = useMemo(() => {
return {
root: null,
rootMargin: "0px",
threshold: 0
};
}, []);
const onVisibilityChangedFn = useCallback(
(entries) => {
const [entry] = entries;
onVisibilityChanged?.(children, entry);
},
[children, onVisibilityChanged]
);
useEffect(() => {
const scoped = wrapperRef.current;
if (!scoped) return;
const observer = new IntersectionObserver(onVisibilityChangedFn, options);
observer.observe(scoped);
return () => {
observer.unobserve(scoped);
};
}, [options, onVisibilityChangedFn]);
useEffect(() => {
return () => onElementDestroyed?.(children);
}, [onElementDestroyed, children]);
return <div ref={wrapperRef}>{children}</div>;
};
export default IntersectionObserverWrapper;
actual component:
import React, { useCallback, useRef } from "react";
import IntersectionObserverWrapper from "../Wrappers/IntersectionObserverWrapper";
import "./IntersectionList.css";
const IntersectionList = ({ items }) => {
const visibilityMap = useRef(new Map());
const clearEntry = (element) => {
const entry = visibilityMap.current.get(element);
if (!entry) return;
const intervalId = entry.intervalId;
clearInterval(intervalId);
visibilityMap.current.delete(element);
};
const onVisibilityChanged = useCallback((element, entry) => {
const { isIntersecting } = entry;
if (isIntersecting) {
const intervalId = setInterval(() => {
const entry = visibilityMap.current.get(element);
if (!entry) {
console.warn("Something is wrong with Map and Interval");
return;
}
console.log(element);
}, 5000);
visibilityMap.current.set(element, {
lastSeenMs: Date.now(),
intervalId: intervalId
});
} else {
clearEntry(element);
}
}, []);
const onElementDestroyed = useCallback((element) => {
clearEntry(element);
}, []);
return (
<ul>
{items.map((x) => (
<IntersectionObserverWrapper
key={x.id}
onVisibilityChanged={onVisibilityChanged}
onElementDestroyed={onElementDestroyed}
>
<div className="obs-block" key={x.id}>
{x.id} - {x.title}
</div>
</IntersectionObserverWrapper>
))}
</ul>
);
};
export default IntersectionList;

I left the key for wrapped divs
just in order to track console log entries.