Scroll to anchor on dynamic interfaces

Scroll to anchor is a technique widely used to move the scroll position to an specific element that has an id assigned. This can have a lot of use cases and you can find it in landing pages, articles with multiple headings, and any other use that you can come up with right now.

The code is really simple to achieve this

<a href="#second-header">Go to second header</a>
<h2 id="second-header">My second header</h2>

If you click the anchor link the browser will scroll to where the second header is located.

This is simple until the dom in the first load does not have the element with the id assigned. If it's not present the user will not scroll to anywhere. And the actual problem is that if the element is dinamically injected to the page using javascript the browser will not scroll either.

In my quest to achieve a solution to this I came up with this reusable react hook:

import { useLayoutEffect } from "react";
import { useLocation } from "react-router-dom";

export function useAsyncScrollAnchoring() {
  const { hash } = useLocation();
  useLayoutEffect(() => {
    if (!hash) return () => {};
    const getLinkedElement = () => document.getElementById(hash.substr(1));
    let element = getLinkedElement();
    if (element) {
      element.scrollIntoView();
      return () => {};
    } else {
      const onMutation: MutationCallback = (entries, observer) => {
        const element = getLinkedElement();
        if (element) {
          element.scrollIntoView();
          observer.disconnect();
        }
      };
      const mutationObserver = new MutationObserver(onMutation);
      mutationObserver.observe(document.body, {
        subtree: true,
        childList: true,
      });
      return () => {
        mutationObserver.disconnect();
      };
    }
  }, [hash]);
}

What this does is, whenever react finishes all dom mutations, the effect is ran and check if there is a hash set first, if there isn't stop there and we are good to go. If that's not the case we check if the element is already in the page and scroll into it, this behavior is native on all browsers, however if the content is displayed asynchronously we still need to do this ourselves. Then, if it's not there already we have to listen for mutations in the document, so for every dom mutation that happens inside the body (perhaps it would be better to attach it to the react root but I'd leave that to your own choice) we check if exists an element with the id equal to the hash, in that case scroll that element into the view and disconnect the observer. Otherwise, do nothing and keep listening.

I'll leave a codesandbox here demonstrating the use case of this.

That's it then, hope this trick would be useful to any of you.

Until next time ๐Ÿ‘‹