· 3 min read

A modern useHash hook

A piece of code importing and using the useHash hook

Note: for impatient readers, you can find the final code at the end of the article.

I've recently been asked to update the hash in a url in a next.js app and I wondered if there was a hook in next/navigation to do that.

Unfortunately, there isn't one, so I naturally googled for a useHash hook.

I found a few, but they were all using useState to store the hash and useEffect to update the hash.

It felt wrong, because I read on thisweekinreact that there was a special hook called useSyncExternalStore that lets you subscribe to an external store.

So, I wondered if I could use it to create a useHash hook.

From my readings, useSyncExternalStore first argument is a subscribe function that takes a callback to call when the watched value changes. Here, we want to watch the hash change, so we need to subscribe to the hashchange event :

function subscribe(onStoreChange: () => void): () => void {
  global.window.addEventListener('hashchange', onStoreChange);
 
  return () => global.window.removeEventListener('hashchange', onStoreChange);
}

As you can see, just like useEffect, we return a "cleanup" function that will unsubscribe from the hashchange event when called.

The second argument is a function that returns a value to be used as the store value. In our case, we want to return the current hash, which we get from window.location.hash.

The third argument is an optional getServerSnapshot function that we won't be using as hash is a client-side only feature. We will return undefined from it.

Let's start to build our hook :

function useHash() {
  const hash = useSyncExternalStore(
    subscribe,
    () => global.window.location.hash,
    () => undefined,
  );
  return hash;
}

This code will return the current hash, and update it when the hash changes.

But it doesn't let us set the hash, so let's add a setHash function to our hook :

const setHash = useCallback((newHash: string): void => {
  global.window.location.hash = newHash;
}, []);

Now, we can return an array with the hash and the setHash function :

return [hash, setHash];

This allow the hook user to get the hash and set the hash in a useState-like fashion.

const [hash, setHash] = useHash();

The final code put all together :

import { useCallback, useSyncExternalStore } from 'react';
 
function subscribe(onStoreChange: () => void): () => void {
  global.window.addEventListener('hashchange', onStoreChange);
 
  return () => global.window.removeEventListener('hashchange', onStoreChange);
}
 
export function useHash(): [string | undefined, (newHash: string) => void] {
  const hash = useSyncExternalStore(
    subscribe,
    () => global.window.location.hash,
    () => undefined,
  );
 
  const setHash = useCallback((newHash: string): void => {
    global.window.location.hash = newHash;
  }, []);
 
  return [hash, setHash];
}

That's how you get a modern useHash hook that updates the hash when it changes and let you set the hash, free from the need to use useState and useEffect!