table of contents with IntersectionOvserver (not perfect)

useScrollSpy.ts

import { useEffect, useRef, useState } from "react";

export function useScrollSpy(ids: string[]) {
  const [activeId, setActiveId] = useState();
  const observer = useRef();

  useEffect(() => {
    const elements = ids.map((id) => document.getElementById(id));

    observer.current?.disconnect();

    elements.forEach((heading) => {
      if (heading) {
        observer.current?.observe(heading);
      }
    });

    observer.current = new IntersectionObserver(
      (entries) => {
        console.group("executing intersectionobserver callback");

        entries.forEach((entry) => {
          if (entry?.isIntersecting) {
            console.log(entry.target.id, "is intersecting");

            setActiveId(entry.target.id);
          }
        });
        console.groupEnd();
      },
      { rootMargin: "0% 0% -80% 0%", threshold: 0.1 }
    );

    return () => observer.current?.disconnect();
  }, [ids]);
  return activeId;
}

TableOfContents.tsx

import React, { useEffect, useState } from "react";
import classes from "./TableOfContents.module.css";
import { useScrollSpy } from "@/hooks/useScrollSpy";

interface TableOfContentsProps {
  isLoading: boolean;
}

export type heading = { text: string | null; id: string };

export const TableOfContents = ({ isLoading }: TableOfContentsProps) => {
  const [headings, setHeadings] = useState<heading[]>([]);
  const activeId = useScrollSpy(headings.map(({ id }) => id));
  useEffect(() => {
    if (!isLoading) {
      const elements: heading[] = Array.from(
        document.querySelectorAll("h3")
      ).map((element) => ({ text: element.textContent, id: element.id }));
      setHeadings(elements);
    }
  }, [isLoading]);

  return (
    <div
      className={`${classes.root} d-none d-lg-block sticky-with-gutter list-group small`}
    >
      <nav>
        {headings.map((heading) => {
          const maybeActive = activeId === heading.id ? "active" : "";
          return (
            <li key={heading.id}>
              <a
                className={`list-group-item list-group-item-action ${maybeActive}`}
                href={`#${heading.id}`}
              >
                {heading.text}
              </a>
            </li>
          );
        })}
      </nav>
    </div>
  );
};