DomanskaGrzyb
DomanskaGrzyb

Reputation: 38

Delay Intersection Observer in React

My goal: Console log elements only if they are still visible

I have a list of elements that I'm passing to my component.

const list = [{id: 0, title: 'Title 0'}, {id: 1, title: 'Title 1'}, {id: 2, title: 'Title 2'}]

I want to render that list and observe each element.

const MyComponent = ({ list }) => {
  const itemsRef = useRef([]);

  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        setTimeout(() => {
          // check if element is still visible, if so - console.log it
        }, 5000);
      }
    });
  });

  itemsRef.current.forEach(ref => observer.observe(ref));

  return (
    <>
      <h1>This is my list</h1>

      {list.map((el, index) => (
        <h2
          ref={(htmlEl) => { itemsRef.current[index] = htmlEl }}
          key={el.id}
        >
          {el.title}
        </h2>
      ))}
    </>
  );
};

Then, finally, if element is visible on my screen, I want to start counting time. After specific time, if element is still visible on the screen - I want to console.log it.

The issue is that elements that left my screen still appear as isIntersecting. Also, I don't want to unobserve them when they leave screen, in case I would scroll up and then look at them for a specific time, but we can unobserve elements when they are console.log.

Upvotes: 2

Views: 1168

Answers (1)

Sergey Sosunov
Sergey Sosunov

Reputation: 4600

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;

Edit React IntersectionObserver

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

Upvotes: 2

Related Questions