Skip to content

Commit bb83675

Browse files
committed
#346 🧊 add useUrlSearchParams hook
1 parent 145f483 commit bb83675

File tree

5 files changed

+269
-0
lines changed

5 files changed

+269
-0
lines changed

‎packages/core/src/bundle/hooks/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export * from './useTimeout/useTimeout';
125125
export * from './useTimer/useTimer';
126126
export * from './useToggle/useToggle';
127127
export * from './useUnmount/useUnmount';
128+
export * from './useUrlSearchParams/useUrlSearchParams';
128129
export * from './useVibrate/useVibrate';
129130
export * from './useWakeLock/useWakeLock';
130131
export * from './useWebSocket/useWebSocket';
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { useEffect, useState } from 'react';
2+
/**
3+
* @name useUrlSearchParams
4+
* @description - Hook that provides reactive URLSearchParams
5+
* @category Browser
6+
*
7+
* @overload
8+
* @template Value The type of the url param values
9+
* @param {UrlSearchParamsMode} mode The URL mode
10+
* @param {UseUrlSearchParamsOptions<Value>} [options] The URL mode
11+
* @returns {UseUrlSearchParamsReturn<Value>} The object with value and function for change value
12+
*
13+
* @example
14+
* const { value, set } = useUrlSearchParams('history');
15+
*/
16+
export const useUrlSearchParams = (mode = 'history', options = {}) => {
17+
const {
18+
initialValue = {},
19+
removeNullishValues = true,
20+
removeFalsyValues = false,
21+
write = true,
22+
window: defaultWindow = window,
23+
writeMode = 'replace'
24+
} = options;
25+
const getRawParams = () => {
26+
const { search, hash } = defaultWindow.location;
27+
if (mode === 'history') return search;
28+
if (mode === 'hash-params') return hash.replace(/^#/, '');
29+
const index = hash.indexOf('?');
30+
return index > -1 ? hash.slice(index) : '';
31+
};
32+
const urlParamsToObject = (params) => {
33+
const result = {};
34+
for (const key of params.keys()) {
35+
const values = params.getAll(key);
36+
result[key] = values.length > 1 ? values : values[0];
37+
}
38+
return result;
39+
};
40+
const [params, setParams] = useState(() => {
41+
if (!defaultWindow) return initialValue;
42+
return urlParamsToObject(new URLSearchParams(getRawParams()));
43+
});
44+
const buildQueryString = (params) => {
45+
const paramsString = params.toString();
46+
const { search, hash } = defaultWindow.location;
47+
if (mode === 'history') return `${paramsString ? `?${paramsString}` : ''}${hash}`;
48+
if (mode === 'hash-params') return `${search}${paramsString ? `#${paramsString}` : ''}`;
49+
const index = hash.indexOf('?');
50+
const base = index > -1 ? hash.slice(0, index) : hash;
51+
return `${search}${base}${paramsString ? `?${paramsString}` : ''}`;
52+
};
53+
const updateUrl = (newParams) => {
54+
if (!defaultWindow || !write) return;
55+
const searchParams = new URLSearchParams();
56+
Object.entries({ ...params, ...newParams }).forEach(([key, value]) => {
57+
if (value == null && removeNullishValues) return;
58+
if (!value && removeFalsyValues) return;
59+
Array.isArray(value)
60+
? value.forEach((value) => searchParams.append(key, value))
61+
: searchParams.set(key, String(value));
62+
});
63+
const query = buildQueryString(searchParams);
64+
writeMode === 'replace'
65+
? defaultWindow.history.replaceState({}, '', query)
66+
: defaultWindow.history.pushState({}, '', query);
67+
setParams(urlParamsToObject(searchParams));
68+
};
69+
useEffect(() => {
70+
if (!defaultWindow) return;
71+
const currentParams = new URLSearchParams(getRawParams());
72+
if (!Array.from(currentParams.keys()).length && Object.keys(initialValue).length) {
73+
updateUrl(initialValue);
74+
}
75+
const handleSetParams = () => {
76+
const newParams = new URLSearchParams(getRawParams());
77+
setParams(urlParamsToObject(newParams));
78+
};
79+
defaultWindow.addEventListener('popstate', handleSetParams);
80+
if (mode !== 'history') defaultWindow.addEventListener('hashchange', handleSetParams);
81+
return () => {
82+
defaultWindow.removeEventListener('popstate', handleSetParams);
83+
if (mode !== 'history') defaultWindow.removeEventListener('hashchange', handleSetParams);
84+
};
85+
}, [defaultWindow, mode]);
86+
return {
87+
value: params,
88+
set: updateUrl
89+
};
90+
};

‎packages/core/src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export * from './useTimeout/useTimeout';
125125
export * from './useTimer/useTimer';
126126
export * from './useToggle/useToggle';
127127
export * from './useUnmount/useUnmount';
128+
export * from './useUrlSearchParams/useUrlSearchParams';
128129
export * from './useVibrate/useVibrate';
129130
export * from './useWakeLock/useWakeLock';
130131
export * from './useWebSocket/useWebSocket';
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useUrlSearchParams } from './useUrlSearchParams';
2+
3+
const Demo = () => {
4+
const urlSearchParams = useUrlSearchParams('history', {
5+
initialValue: { param1: 'value1', param2: 'value2' },
6+
writeMode: 'push'
7+
});
8+
9+
const onUrlSearchParamAdd = () => {
10+
const paramCount = Object.keys(urlSearchParams.value).length + 1;
11+
urlSearchParams.set({ [`param${paramCount}`]: `value${paramCount}` });
12+
};
13+
14+
return (
15+
<div className='flex flex-col gap-3'>
16+
{Object.entries(urlSearchParams.value).map(([key, value]) => (
17+
<div key={key} className='flex items-center gap-3'>
18+
<p>{key}:</p>
19+
<input
20+
className='w-fit'
21+
value={value}
22+
onChange={(event) => urlSearchParams.set({ [key]: event.target.value })}
23+
placeholder='Type value for url param'
24+
/>
25+
</div>
26+
))}
27+
<button className='w-30' type='button' onClick={onUrlSearchParamAdd}>
28+
Add
29+
</button>
30+
</div>
31+
);
32+
};
33+
34+
export default Demo;
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { useEffect, useState } from 'react';
2+
3+
export type UrlParams = Record<string, string | string[]>;
4+
export type UrlSearchParamsMode = 'hash-params' | 'hash' | 'history';
5+
6+
export interface UseUrlSearchParamsOptions<VALUE> {
7+
/** The data types for drop zone */
8+
initialValue?: VALUE;
9+
/** The data types for drop zone */
10+
removeFalsyValues?: boolean;
11+
/** The data types for drop zone */
12+
removeNullishValues?: boolean;
13+
/** The data types for drop zone */
14+
window?: Window;
15+
/** The data types for drop zone */
16+
write?: boolean;
17+
/** The data types for drop zone */
18+
writeMode?: 'push' | 'replace';
19+
}
20+
21+
export interface UseUrlSearchParamsReturn<VALUE> {
22+
value: VALUE;
23+
set: (newParams: Partial<VALUE>) => void;
24+
}
25+
26+
/**
27+
* @name useUrlSearchParams
28+
* @description - Hook that provides reactive URLSearchParams
29+
* @category Browser
30+
*
31+
* @overload
32+
* @template Value The type of the url param values
33+
* @param {UrlSearchParamsMode} mode The URL mode
34+
* @param {UseUrlSearchParamsOptions<Value>} [options] The URL mode
35+
* @returns {UseUrlSearchParamsReturn<Value>} The object with value and function for change value
36+
*
37+
* @example
38+
* const { value, set } = useUrlSearchParams('history');
39+
*/
40+
41+
export const useUrlSearchParams = <VALUE extends Record<string, any> = UrlParams>(
42+
mode: UrlSearchParamsMode = 'history',
43+
options: UseUrlSearchParamsOptions<VALUE> = {}
44+
): UseUrlSearchParamsReturn<VALUE> => {
45+
const {
46+
initialValue = {},
47+
removeNullishValues = true,
48+
removeFalsyValues = false,
49+
write = true,
50+
window: defaultWindow = window,
51+
writeMode = 'replace'
52+
} = options;
53+
54+
const getRawParams = () => {
55+
const { search, hash } = defaultWindow.location;
56+
57+
if (mode === 'history') return search;
58+
if (mode === 'hash-params') return hash.replace(/^#/, '');
59+
60+
const index = hash.indexOf('?');
61+
return index > -1 ? hash.slice(index) : '';
62+
};
63+
64+
const urlParamsToObject = (params: URLSearchParams) => {
65+
const result: UrlParams = {};
66+
67+
for (const key of params.keys()) {
68+
const values = params.getAll(key);
69+
result[key] = values.length > 1 ? values : values[0];
70+
}
71+
72+
return result;
73+
};
74+
75+
const [params, setParams] = useState<VALUE>(() => {
76+
if (!defaultWindow) return initialValue as VALUE;
77+
return urlParamsToObject(new URLSearchParams(getRawParams())) as VALUE;
78+
});
79+
80+
const buildQueryString = (params: URLSearchParams): string => {
81+
const paramsString = params.toString();
82+
const { search, hash } = defaultWindow.location;
83+
84+
if (mode === 'history') return `${paramsString ? `?${paramsString}` : ''}${hash}`;
85+
if (mode === 'hash-params') return `${search}${paramsString ? `#${paramsString}` : ''}`;
86+
87+
const index = hash.indexOf('?');
88+
const base = index > -1 ? hash.slice(0, index) : hash;
89+
90+
return `${search}${base}${paramsString ? `?${paramsString}` : ''}`;
91+
};
92+
93+
const updateUrl = (newParams: Partial<VALUE>) => {
94+
if (!defaultWindow || !write) return;
95+
96+
const searchParams = new URLSearchParams();
97+
98+
Object.entries({ ...params, ...newParams }).forEach(([key, value]) => {
99+
if (value == null && removeNullishValues) return;
100+
if (!value && removeFalsyValues) return;
101+
102+
Array.isArray(value)
103+
? value.forEach((value) => searchParams.append(key, value))
104+
: searchParams.set(key, String(value));
105+
});
106+
107+
const query = buildQueryString(searchParams);
108+
109+
writeMode === 'replace'
110+
? defaultWindow.history.replaceState({}, '', query)
111+
: defaultWindow.history.pushState({}, '', query);
112+
113+
setParams(urlParamsToObject(searchParams) as VALUE);
114+
};
115+
116+
useEffect(() => {
117+
if (!defaultWindow) return;
118+
119+
const currentParams = new URLSearchParams(getRawParams());
120+
121+
if (!Array.from(currentParams.keys()).length && Object.keys(initialValue).length) {
122+
updateUrl(initialValue);
123+
}
124+
125+
const handleSetParams = () => {
126+
const newParams = new URLSearchParams(getRawParams());
127+
setParams(urlParamsToObject(newParams) as VALUE);
128+
};
129+
130+
defaultWindow.addEventListener('popstate', handleSetParams);
131+
if (mode !== 'history') defaultWindow.addEventListener('hashchange', handleSetParams);
132+
133+
return () => {
134+
defaultWindow.removeEventListener('popstate', handleSetParams);
135+
if (mode !== 'history') defaultWindow.removeEventListener('hashchange', handleSetParams);
136+
};
137+
}, [defaultWindow, mode]);
138+
139+
return {
140+
value: params,
141+
set: updateUrl
142+
};
143+
};

0 commit comments

Comments
 (0)