A modern 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
!