1

When someone manually modifies the URL hash (aka the URI fragment identifier), how can I trigger a re-render?

I have a custom hook looking like this:

function useUrlHash(): string | null {
  const router = useRouter();

  useEffect(() => {
    console.log("the effect ran!");
  }, [router.asPath])


  if (!router.isReady) {
    return null;
  }

  return router.asPath.split("#")[1];
}

I expect that when router.asPath changes (such as the user manually changing the hash and pressing Enter), the effect should run, thus triggering a re-render with the new value return by useUrlHash.

But it does not. Why? And what's the simplest way to trigger re-render? Ideally without holding on to the URL hash state in React (so single source of truth is in the URL itself).

Garrett
  • 4,007
  • 2
  • 41
  • 59
  • 2
    Does [this](https://stackoverflow.com/questions/69343932/how-to-detect-change-in-the-url-hash-in-next-js) answer your question? – ivanatias Nov 12 '22 at 21:18
  • Ah yes, the [second answer](https://stackoverflow.com/a/72700035/2223706) there deals with hash changes, including manual changes. – Garrett Nov 12 '22 at 22:31

1 Answers1

1

Here's the simplest solution I was able to come up with. It handles programmatic hash changes as well as directly editing the URL.

import {useEffect, useReducer} from "react";

function useUrlHash(): string | null {
  const forceUpdate = useForceUpdate()

  useEffect(() => {
    function handleHashChange() {
      forceUpdate();
    }
    addEventListener("hashchange", handleHashChange);

    return () => {
      removeEventListener("hashchange", handleHashChange);
    };
  }, [forceUpdate]);

  if (typeof window === "undefined") {
    // Return `null` while server-side
    return null;
  }

  return window.location.hash.split("#")[1];
}

function useForceUpdate(): () => void {
  // This is the recommended escape hatch you can use, as described here:
  // https://reactjs.org/docs/hooks-faq.html#is-there-something-like-forceupdate
  // They do suggest avoiding this, so only use this sparingly.
  const [, forceUpdate] = useReducer((dummyVar: number) => dummyVar + 1, 0);
  return forceUpdate;
}

We wanted to have a single source-of-truth for the hash (instead of living in both the URL and React state, as per this video), so we use the forceUpdate pattern. Another pattern is shown here.

Garrett
  • 4,007
  • 2
  • 41
  • 59