Skip to content

Commit 4be1c2a

Browse files
authored
Merge pull request #350 from siberiacancode/#346
[feat]: useUrlSearchParams
2 parents 145f483 + 6a5c051 commit 4be1c2a

File tree

5 files changed

+272
-0
lines changed

5 files changed

+272
-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: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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 initial value for hook that use in URL params */
8+
initialValue?: VALUE;
9+
/** The boolean flag, for remove values that has falsy values (e.g. `''`, `0`, `false`, `NaN`) */
10+
removeFalsyValues?: boolean;
11+
/** The boolean flag, for remove values that 'null' and 'undefined' */
12+
removeNullishValues?: boolean;
13+
/** The custom window object */
14+
window?: Window;
15+
/** Whether to write changes back to the URL */
16+
write?: boolean;
17+
/**
18+
* Use `'push'` to push a new history entry on each update,
19+
* or `'replace'` to replace the current entry.
20+
*/
21+
writeMode?: 'push' | 'replace';
22+
}
23+
24+
export interface UseUrlSearchParamsReturn<VALUE> {
25+
value: VALUE;
26+
set: (newParams: Partial<VALUE>) => void;
27+
}
28+
29+
/**
30+
* @name useUrlSearchParams
31+
* @description - Hook that provides reactive URLSearchParams
32+
* @category Browser
33+
*
34+
* @overload
35+
* @template Value The type of the url param values
36+
* @param {UrlSearchParamsMode} mode The URL mode
37+
* @param {UseUrlSearchParamsOptions<Value>} [options] The URL mode
38+
* @returns {UseUrlSearchParamsReturn<Value>} The object with value and function for change value
39+
*
40+
* @example
41+
* const { value, set } = useUrlSearchParams('history');
42+
*/
43+
44+
export const useUrlSearchParams = <VALUE extends Record<string, any> = UrlParams>(
45+
mode: UrlSearchParamsMode = 'history',
46+
options: UseUrlSearchParamsOptions<VALUE> = {}
47+
): UseUrlSearchParamsReturn<VALUE> => {
48+
const {
49+
initialValue = {},
50+
removeNullishValues = true,
51+
removeFalsyValues = false,
52+
write = true,
53+
window: defaultWindow = window,
54+
writeMode = 'replace'
55+
} = options;
56+
57+
const getRawParams = () => {
58+
const { search, hash } = defaultWindow.location;
59+
60+
if (mode === 'history') return search;
61+
if (mode === 'hash-params') return hash.replace(/^#/, '');
62+
63+
const index = hash.indexOf('?');
64+
return index > -1 ? hash.slice(index) : '';
65+
};
66+
67+
const urlParamsToObject = (params: URLSearchParams) => {
68+
const result: UrlParams = {};
69+
70+
for (const key of params.keys()) {
71+
const values = params.getAll(key);
72+
result[key] = values.length > 1 ? values : values[0];
73+
}
74+
75+
return result;
76+
};
77+
78+
const [params, setParams] = useState<VALUE>(() => {
79+
if (!defaultWindow) return initialValue as VALUE;
80+
return urlParamsToObject(new URLSearchParams(getRawParams())) as VALUE;
81+
});
82+
83+
const buildQueryString = (params: URLSearchParams): string => {
84+
const paramsString = params.toString();
85+
const { search, hash } = defaultWindow.location;
86+
87+
if (mode === 'history') return `${paramsString ? `?${paramsString}` : ''}${hash}`;
88+
if (mode === 'hash-params') return `${search}${paramsString ? `#${paramsString}` : ''}`;
89+
90+
const index = hash.indexOf('?');
91+
const base = index > -1 ? hash.slice(0, index) : hash;
92+
93+
return `${search}${base}${paramsString ? `?${paramsString}` : ''}`;
94+
};
95+
96+
const updateUrl = (newParams: Partial<VALUE>) => {
97+
if (!defaultWindow || !write) return;
98+
99+
const searchParams = new URLSearchParams();
100+
101+
Object.entries({ ...params, ...newParams }).forEach(([key, value]) => {
102+
if (value == null && removeNullishValues) return;
103+
if (!value && removeFalsyValues) return;
104+
105+
Array.isArray(value)
106+
? value.forEach((value) => searchParams.append(key, value))
107+
: searchParams.set(key, String(value));
108+
});
109+
110+
const query = buildQueryString(searchParams);
111+
112+
writeMode === 'replace'
113+
? defaultWindow.history.replaceState({}, '', query)
114+
: defaultWindow.history.pushState({}, '', query);
115+
116+
setParams(urlParamsToObject(searchParams) as VALUE);
117+
};
118+
119+
useEffect(() => {
120+
if (!defaultWindow) return;
121+
122+
const currentParams = new URLSearchParams(getRawParams());
123+
124+
if (!Array.from(currentParams.keys()).length && Object.keys(initialValue).length) {
125+
updateUrl(initialValue);
126+
}
127+
128+
const handleSetParams = () => {
129+
const newParams = new URLSearchParams(getRawParams());
130+
setParams(urlParamsToObject(newParams) as VALUE);
131+
};
132+
133+
defaultWindow.addEventListener('popstate', handleSetParams);
134+
if (mode !== 'history') defaultWindow.addEventListener('hashchange', handleSetParams);
135+
136+
return () => {
137+
defaultWindow.removeEventListener('popstate', handleSetParams);
138+
if (mode !== 'history') defaultWindow.removeEventListener('hashchange', handleSetParams);
139+
};
140+
}, [defaultWindow, mode]);
141+
142+
return {
143+
value: params,
144+
set: updateUrl
145+
};
146+
};

0 commit comments

Comments
 (0)