18

How do we detect a change in the URL hash of a Next.js project?

I don't want to reload my page every time the slug changes.

I cannot use <Link> since all of my data comes from DB

Example: When clicking on an tag from
http://example/test#url1
to
http://example.com/test#url2

Tried the below, but this seems to work for path change only.

import React, { useEffect,useState } from 'react';
import { useRouter } from 'next/router'

const test = () => {
    const router = useRouter();

    useEffect(() => {
        console.log(router.asPath);
    }, [router.asPath]);

    return (<></>);
};

export default test;

5 Answers 5

26

You can listen to hash changes using hashChangeStart event from router.events.

const Test = () => {
    const router = useRouter();

    useEffect(() => {
        const onHashChangeStart = (url) => {
            console.log(`Path changing to ${url}`);
        };

        router.events.on("hashChangeStart", onHashChangeStart);

        return () => {
            router.events.off("hashChangeStart", onHashChangeStart);
        };
    }, [router.events]);

    return (
        <>
            <Link href="/#some-hash">
                <a>Link to #some-hash</a>
            </Link>
            <Link href="/#some-other-hash">
                <a>Link to #some-other-hash</a>
            </Link>
        </>
    );
};

If you're not using next/link or next/router for client-side navigation (not recommended in Next.js apps), then you'll need to listen to the window's hashchange event.

Your useEffect would look like the following.

useEffect(() => {
    const onHashChanged = () => {
        console.log('Hash changed');
    };

    window.addEventListener("hashchange", onHashChanged);

    return () => {
        window.removeEventListener("hashchange", onHashChanged);
    };
}, []);
Sign up to request clarification or add additional context in comments.

6 Comments

Thanks @juliomalves, I cannot use <Link> since all of my html comes from database
I will update the question
Without <Link> the events are not triggered in my app
If you want the events to be picked up by next/router then you need to use next/link or next/router for client-side navigations. Otherwise, you'll need to add an event listener for the window's hashchange event. I'll update my answer.
Note that NextJS hashChangeStart Event does not account for browser refresh or navigation, see my answer below..
|
11

If you're relying on URL hash for multiple re-renders or state changes, note that NextJS hashChangeStart event does not account for browser refresh or direct browser URL address navigation

A complete solution might need a combination of event listeners to cover all edge cases.

const useUrlHash = (initialValue) => {
  const router = useRouter()
  const [hash, setHash] = useState(initialValue)

  const updateHash = (str) => {
    if (!str) return
    setHash(str.split('#')[1])
  }

  useEffect(() => {
    const onWindowHashChange = () => updateHash(window.location.hash)
    const onNextJSHashChange = (url) => updateHash(url)

    router.events.on('hashChangeStart', onNextJSHashChange)
    window.addEventListener('hashchange', onWindowHashChange)
    window.addEventListener('load', onWindowHashChange)
    return () => {
      router.events.off('hashChangeStart', onNextJSHashChange)
      window.removeEventListener('load', onWindowHashChange)
      window.removeEventListener('hashchange', onWindowHashChange)
    }
  }, [router.asPath, router.events])

  return hash
}

Comments

3

Next.js App Router (Next.js version 13 and above):

We can use a Hook like this:

'use client'

import { useParams } from 'next/navigation'
import { useEffect, useState } from 'react'

const getHash = () =>
  typeof window !== 'undefined' ? window.location.hash : ''

const useHash = () => {
  const [isClient, setIsClient] = useState(false)
  const [hash, setHash] = useState<string>(getHash())
  const params = useParams()

  useEffect(() => {
    setIsClient(true)
    setHash(getHash())
  }, [params])

  return isClient ? hash : ''
}

export default useHash

How to use:

'use client'

import useHash from './useHash'

const HashDisplay = () => {
  const hash = useHash()

  return (
    <div>
      <h2>Current Hash:</h2>
      {hash ? (
        <p>{hash}</p>
      ) : (
        <p>No hash present in the URL</p>
      )}
    </div>
  )
}

export default HashDisplay

2 Comments

I too use this in Next 14.2, doesn't work for me in Next 15.1 tho...
@ΔO what's wrong with it? is there any error or something?
3

router.events have been deprecated/removed since NextJS 14 for who knows what reason https://github.com/vercel/next.js/discussions/42016 So the accepted answer doesn't work anymore. hashchange events don't also work when hash is changed from next/link Link click since it's done through window.pushState.

So what I did was hack together this monstrosity:

'use client'

import React from 'react'

export const useHash = () => {
    const [hash, setHash] = React.useState(window.location.hash)
    React.useEffect(() => {
        const onHashChanged = () => setHash(window.location.hash)
        const { pushState, replaceState } = window.history
        window.history.pushState = function (...args) {
            pushState.apply(window.history, args)
            setTimeout(() => setHash(window.location.hash))
        }
        window.history.replaceState = function (...args) {
            replaceState.apply(window.history, args)
            setTimeout(() => setHash(window.location.hash))
        }
        window.addEventListener('hashchange', onHashChanged)
        return () => {
            window.removeEventListener('hashchange', onHashChanged)
        }
    }, [])
    return hash
}

Which ain't great but seems to work with both Link clicks and direct changes to hash that trigger hashchange events (but not pushState).

3 Comments

You're a genius. Posting this on GitHub Discussion for visibility github.com/vercel/next.js/discussions/…
Thanks. Looking back at it now months later, I'd probably clean up the monkey-patching as well incase you have multiple components using this. OR, even better, store it in global store in which case you don't have to.
@TeemuK, happy to see another iteration of this :)
0

I use this workaround in Next.js 15 until there is an official Client Component hook to handle it.

const params = useParams();
const [url, setURL] = useState<string>('')
useEffect(() => {
    const update = () => {
        // set url from path in forward.
        setURL(window.location.href.substring(window.location.href.indexOf('/', 9)))
    };
    window.addEventListener('hashchange', update);
    update(); // Initial fetch
    return () => window.removeEventListener('hashchange', update)
}, [params])
console.log(url)

The url variable contains the root path with the current URL fragment (#).

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.