Skip to content

Commit 12fe26f

Browse files
committed
main 🧊 add callback for use hash
1 parent 88ea88a commit 12fe26f

File tree

3 files changed

+158
-20
lines changed

3 files changed

+158
-20
lines changed
Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,62 @@
1-
import { useEffect, useState } from 'react';
2-
const getHash = () => decodeURIComponent(window.location.hash.replace('#', ''));
1+
import { useEffect, useRef, useState } from 'react';
2+
export const getHash = () => decodeURIComponent(window.location.hash.replace('#', ''));
33
/**
44
* @name useHash
55
* @description - Hook that manages the hash value
66
* @category State
77
* @usage low
88
*
9+
* @overload
910
* @param {string} [initialValue] The initial hash value if no hash exists
11+
* @param {UseHashOptions} [options] Configuration options
12+
* @param {boolean} [options.enabled] The enabled state of the hook
13+
* @param {'initial' | 'replace'} [options.mode] The mode of hash setting
14+
* @param {(hash: string) => void} [options.onChange] Callback function called when hash changes
1015
* @returns {UseHashReturn} An array containing the hash value and a function to set the hash value
1116
*
1217
* @example
13-
* const [hash, setHash] = useHash("initial");
18+
* const [hash, setHash] = useHash("initial", {
19+
* enabled: true,
20+
* mode: "replace",
21+
* onChange: (newHash) => console.log('Hash changed:', newHash)
22+
* });
23+
*
24+
* @overload
25+
* @param {string} [initialValue] The initial hash value if no hash exists
26+
* @param {(hash: string) => void} [callback] Callback function called when hash changes
27+
* @returns {UseHashReturn} An array containing the hash value and a function to set the hash value
28+
*
29+
* @example
30+
* const [hash, setHash] = useHash("initial", (newHash) => console.log('Hash changed:', newHash));
1431
*/
15-
export const useHash = (initialValue = '', mode = 'replace') => {
32+
export const useHash = (...params) => {
33+
const [initialValue = '', param] = params;
34+
const options = typeof param === 'function' ? { onChange: param } : param;
35+
const enabled = options?.enabled ?? true;
36+
const mode = options?.mode ?? 'replace';
1637
const [hash, setHash] = useState(() => {
1738
if (typeof window === 'undefined') return initialValue;
1839
return getHash() || initialValue;
1940
});
41+
const optionsRef = useRef(options);
42+
optionsRef.current = options;
2043
const set = (value) => {
2144
window.location.hash = value;
2245
setHash(value);
46+
optionsRef.current?.onChange?.(value);
2347
};
2448
useEffect(() => {
49+
if (!enabled) return;
2550
if (mode === 'replace') window.location.hash = hash;
26-
const onHashChange = () => setHash(getHash());
51+
const onHashChange = () => {
52+
const newHash = getHash();
53+
setHash(newHash);
54+
optionsRef.current?.onChange?.(newHash);
55+
};
2756
window.addEventListener('hashchange', onHashChange);
2857
return () => {
2958
window.removeEventListener('hashchange', onHashChange);
3059
};
31-
}, []);
60+
}, [enabled, mode]);
3261
return [hash, set];
3362
};

‎packages/core/src/hooks/useHash/useHash.test.ts‎

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ it('Should decode hash', () => {
5656

5757
const [_, set] = result.current;
5858

59-
act(() => set('test value'));
59+
act(() => set('testvalue'));
6060

61-
expect(window.location.hash).toBe('#test%20value');
62-
expect(result.current[0]).toBe('test value');
61+
expect(window.location.hash).toBe('#testvalue');
62+
expect(result.current[0]).toBe('testvalue');
6363
});
6464

6565
it('Should use initial value', () => {
@@ -72,12 +72,71 @@ it('Should use initial value', () => {
7272
it('Should prefer existing hash over initial value', () => {
7373
window.location.hash = '#existing';
7474

75-
const { result } = renderHook(() => useHash('initial', 'initial'));
75+
const { result } = renderHook(() => useHash('initial', { mode: 'initial' }));
7676

7777
expect(window.location.hash).toBe('#existing');
7878
expect(result.current[0]).toBe('existing');
7979
});
8080

81+
it('Should call onChange callback when hash changes programmatically', () => {
82+
const callback = vi.fn();
83+
const { result } = renderHook(() => useHash('initial', callback));
84+
85+
const [_, set] = result.current;
86+
87+
act(() => set('testvalue'));
88+
89+
expect(callback).toHaveBeenCalledWith('testvalue');
90+
expect(callback).toHaveBeenCalledTimes(1);
91+
});
92+
93+
it('Should call onChange callback when hash changes externally', () => {
94+
const callback = vi.fn();
95+
renderHook(() => useHash('initial', callback));
96+
97+
act(() => {
98+
window.location.hash = '#external-hash';
99+
window.dispatchEvent(new Event('hashchange'));
100+
});
101+
102+
expect(callback).toHaveBeenCalledWith('external-hash');
103+
});
104+
105+
it('Should work with options object', () => {
106+
const onChange = vi.fn();
107+
const { result } = renderHook(() =>
108+
useHash('initial', {
109+
onChange
110+
})
111+
);
112+
113+
const [_, set] = result.current;
114+
115+
expect(result.current[0]).toBe('initial');
116+
117+
act(() => set('testvalue'));
118+
119+
expect(onChange).toHaveBeenCalledWith('testvalue');
120+
expect(result.current[0]).toBe('testvalue');
121+
});
122+
123+
it('Should not work when disabled', () => {
124+
const { result } = renderHook(() =>
125+
useHash('initial', {
126+
enabled: false
127+
})
128+
);
129+
130+
expect(result.current[0]).toBe('initial');
131+
132+
act(() => {
133+
window.location.hash = '#external-hash';
134+
window.dispatchEvent(new Event('hashchange'));
135+
});
136+
137+
expect(result.current[0]).toBe('initial');
138+
});
139+
81140
it('Should clean up on unmount', () => {
82141
const { unmount } = renderHook(useHash);
83142
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,95 @@
1-
import { useEffect, useState } from 'react';
1+
import { useEffect, useRef, useState } from 'react';
22

3-
const getHash = () => decodeURIComponent(window.location.hash.replace('#', ''));
3+
export const getHash = () => decodeURIComponent(window.location.hash.replace('#', ''));
4+
5+
/** The use hash options type */
6+
export interface UseHashOptions {
7+
/** The enabled state of the hook */
8+
enabled?: boolean;
9+
/** The mode of hash setting */
10+
mode?: 'initial' | 'replace';
11+
/** Callback function called when hash changes */
12+
onChange?: (hash: string) => void;
13+
}
414

515
/** The use hash return type */
616
type UseHashReturn = [string, (value: string) => void];
717

18+
export interface UseHash {
19+
(initialValue?: string, options?: UseHashOptions): UseHashReturn;
20+
21+
(initialValue?: string, callback?: (hash: string) => void): UseHashReturn;
22+
}
23+
824
/**
925
* @name useHash
1026
* @description - Hook that manages the hash value
1127
* @category State
1228
* @usage low
1329
*
30+
* @overload
31+
* @param {string} [initialValue] The initial hash value if no hash exists
32+
* @param {UseHashOptions} [options] Configuration options
33+
* @param {boolean} [options.enabled] The enabled state of the hook
34+
* @param {'initial' | 'replace'} [options.mode] The mode of hash setting
35+
* @param {(hash: string) => void} [options.onChange] Callback function called when hash changes
36+
* @returns {UseHashReturn} An array containing the hash value and a function to set the hash value
37+
*
38+
* @example
39+
* const [hash, setHash] = useHash("initial", {
40+
* enabled: true,
41+
* mode: "replace",
42+
* onChange: (newHash) => console.log('Hash changed:', newHash)
43+
* });
44+
*
45+
* @overload
1446
* @param {string} [initialValue] The initial hash value if no hash exists
47+
* @param {(hash: string) => void} [callback] Callback function called when hash changes
1548
* @returns {UseHashReturn} An array containing the hash value and a function to set the hash value
1649
*
1750
* @example
18-
* const [hash, setHash] = useHash("initial");
51+
* const [hash, setHash] = useHash("initial", (newHash) => console.log('Hash changed:', newHash));
1952
*/
20-
export const useHash = (
21-
initialValue = '',
22-
mode: 'initial' | 'replace' = 'replace'
23-
): UseHashReturn => {
53+
export const useHash = ((...params: any[]) => {
54+
const [initialValue = '', param] = params;
55+
56+
const options = (typeof param === 'function' ? { onChange: param } : param) as
57+
| UseHashOptions
58+
| undefined;
59+
60+
const enabled = options?.enabled ?? true;
61+
const mode = options?.mode ?? 'replace';
62+
2463
const [hash, setHash] = useState(() => {
2564
if (typeof window === 'undefined') return initialValue;
2665
return getHash() || initialValue;
2766
});
2867

68+
const optionsRef = useRef(options);
69+
optionsRef.current = options;
70+
2971
const set = (value: string) => {
3072
window.location.hash = value;
3173
setHash(value);
74+
optionsRef.current?.onChange?.(value);
3275
};
3376

3477
useEffect(() => {
78+
if (!enabled) return;
79+
3580
if (mode === 'replace') window.location.hash = hash;
3681

37-
const onHashChange = () => setHash(getHash());
82+
const onHashChange = () => {
83+
const newHash = getHash();
84+
setHash(newHash);
85+
optionsRef.current?.onChange?.(newHash);
86+
};
87+
3888
window.addEventListener('hashchange', onHashChange);
3989
return () => {
4090
window.removeEventListener('hashchange', onHashChange);
4191
};
42-
}, []);
92+
}, [enabled, mode]);
4393

4494
return [hash, set] as const;
45-
};
95+
}) as UseHash;

0 commit comments

Comments
 (0)