11'use client' ;
2+ import { usePathname } from 'next/navigation' ;
23import React from 'react' ;
34
45export const HashContext = React . createContext < {
@@ -9,9 +10,17 @@ export const HashContext = React.createContext<{
910 * URL can be relative or absolute.
1011 */
1112 updateHashFromUrl : ( href : string ) => void ;
13+ /**
14+ * Indicates if a link has been clicked recently.
15+ * Becomes true after a click and resets to false when pathname changes.
16+ * It is debounced to avoid flickering on fast navigations.
17+ * Debounce time is 400ms (= doherty threshold for responsiveness).
18+ */
19+ isNavigating : boolean ;
1220} > ( {
1321 hash : null ,
1422 updateHashFromUrl : ( ) => { } ,
23+ isNavigating : false ,
1524} ) ;
1625
1726function getHash ( ) : string | null {
@@ -21,32 +30,74 @@ function getHash(): string | null {
2130 return window . location . hash . slice ( 1 ) ;
2231}
2332
24- export const HashProvider : React . FC < React . PropsWithChildren < { } > > = ( { children } ) => {
33+ export const HashProvider : React . FC < React . PropsWithChildren > = ( { children } ) => {
2534 const [ hash , setHash ] = React . useState < string | null > ( getHash ) ;
35+ const [ isNavigating , setIsNavigating ] = React . useState ( false ) ;
36+ const timeoutRef = React . useRef < number | null > ( null ) ;
37+ const pathname = usePathname ( ) ;
38+ const pathnameRef = React . useRef ( pathname ) ;
39+
40+ // Reset isNavigating when pathname changes
41+ React . useEffect ( ( ) => {
42+ if ( pathnameRef . current !== pathname ) {
43+ setIsNavigating ( false ) ;
44+ if ( timeoutRef . current ) {
45+ clearTimeout ( timeoutRef . current ) ;
46+ timeoutRef . current = null ;
47+ }
48+ pathnameRef . current = pathname ;
49+ }
50+ } , [ pathname ] ) ;
51+
52+ // Cleanup timeout on unmount
53+ React . useEffect ( ( ) => {
54+ return ( ) => {
55+ if ( timeoutRef . current ) {
56+ clearTimeout ( timeoutRef . current ) ;
57+ }
58+ } ;
59+ } , [ ] ) ;
60+
2661 const updateHashFromUrl = React . useCallback ( ( href : string ) => {
2762 const url = new URL (
2863 href ,
2964 typeof window !== 'undefined' ? window . location . origin : 'http://localhost'
3065 ) ;
3166 setHash ( url . hash . slice ( 1 ) ) ;
67+
68+ if ( timeoutRef . current ) {
69+ clearTimeout ( timeoutRef . current ) ;
70+ }
71+ if ( pathnameRef . current !== url . pathname ) {
72+ timeoutRef . current = window . setTimeout ( ( ) => {
73+ setIsNavigating ( true ) ;
74+ timeoutRef . current = null ;
75+ return ;
76+ } , 400 ) ; // 400ms timeout - doherty threshold for responsiveness
77+ }
3278 } , [ ] ) ;
79+
3380 const memoizedValue = React . useMemo (
34- ( ) => ( { hash, updateHashFromUrl } ) ,
35- [ hash , updateHashFromUrl ]
81+ ( ) => ( { hash, updateHashFromUrl, isNavigating } ) ,
82+ [ hash , updateHashFromUrl , isNavigating ]
3683 ) ;
3784 return < HashContext . Provider value = { memoizedValue } > { children } </ HashContext . Provider > ;
3885} ;
3986
4087/**
41- * Hook to get the current hash from the URL.
88+ * Hook to get the current hash from the URL and click state .
4289 * @see https://github.com/vercel/next.js/discussions/49465
4390 * We use a different hack than this one, because for same page link it don't work
4491 * We can't use the `hashChange` event because it doesn't fire for `replaceState` and `pushState` which are used by Next.js.
4592 * Since we have a single Link component that handles all links, we can use a context to share the hash.
4693 */
4794export function useHash ( ) {
48- // const params = useParams();
4995 const { hash } = React . useContext ( HashContext ) ;
5096
5197 return hash ;
5298}
99+
100+ export function useIsNavigating ( ) {
101+ const { isNavigating : hasBeenClicked } = React . useContext ( HashContext ) ;
102+ return hasBeenClicked ;
103+ }
0 commit comments