Skip to content

Commit b0370ab

Browse files
author
babin
committed
main 🧊 rework cookies and params hooks
1 parent 1aa4ebb commit b0370ab

File tree

14 files changed

+772
-254
lines changed

14 files changed

+772
-254
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ export const getCookie = (key) => {
4646
* @param {UseCookieInitialValue<Value>} [initialValue] The initial value of the cookie
4747
* @returns {UseCookieReturn<Value>} The value and the set function
4848
*
49+
* @overload
50+
* @template Value The type of the cookie value
51+
* @param {string} key The key of the cookie
52+
* @param {UseCookieOptions<Value>} options The options object
53+
* @param {UseCookieInitialValue<Value>} [options.initialValue] The initial value of the cookie
54+
* @param {boolean} [options.updateOnChange=true] Whether to update the cookie on change
55+
* @param {(value: string) => Value} [options.deserializer] The deserializer function to be invoked
56+
* @param {(value: Value) => string} [options.serializer] The serializer function to be invoked
57+
* @returns {UseCookieReturn<Value | undefined>} The value and the set function
58+
*
4959
* @example
5060
* const { value, set, remove } = useCookie('key', 'value');
5161
*/

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

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,17 @@
11
import { useEffect, useState } from 'react';
2-
import { COOKIE_EVENT, dispatchCookieEvent, removeCookie, setCookie } from '../useCookie/useCookie';
3-
export const getParsedCookies = () =>
4-
Object.fromEntries(
5-
document.cookie.split('; ').map((cookie) => {
6-
const [key, ...value] = cookie.split('=');
7-
const decodedValue = decodeURIComponent(value.join('='));
8-
try {
9-
return [key, JSON.parse(decodedValue)];
10-
} catch {
11-
return [key, decodedValue];
12-
}
13-
})
14-
);
2+
import {
3+
COOKIE_EVENT,
4+
dispatchCookieEvent,
5+
removeCookie,
6+
removeCookieItem,
7+
setCookieItem
8+
} from '../useCookie/useCookie';
159
export const clearCookies = () => {
1610
document.cookie.split('; ').forEach((cookie) => {
1711
const [name] = cookie.split('=');
1812
removeCookie(name);
1913
});
2014
};
21-
const setCookieItem = (key, value, options) => {
22-
setCookie(key, value, options);
23-
dispatchCookieEvent();
24-
};
25-
const removeCookieItem = (key, options) => {
26-
removeCookie(key, options);
27-
dispatchCookieEvent();
28-
};
2915
const clearCookieItems = () => {
3016
clearCookies();
3117
dispatchCookieEvent();
@@ -37,14 +23,38 @@ const clearCookieItems = () => {
3723
*
3824
* @overload
3925
* @template {object} Value The type of the cookie values
40-
* @param {string} key The key of the cookie
4126
* @returns {UseCookieReturn<Value>} The value and the set function
4227
*
4328
* @example
4429
* const { value, set, remove, getAll, clear } = useCookies();
4530
*/
46-
export const useCookies = () => {
47-
const [value, setValue] = useState(typeof window !== 'undefined' ? getParsedCookies() : {});
31+
export const useCookies = (options) => {
32+
const serializer = (value) => {
33+
if (options?.serializer) return options.serializer(value);
34+
if (typeof value === 'string') return value;
35+
return JSON.stringify(value);
36+
};
37+
const deserializer = (value) => {
38+
if (options?.deserializer) return options.deserializer(value);
39+
if (value === 'undefined') return undefined;
40+
try {
41+
return JSON.parse(value);
42+
} catch {
43+
return value;
44+
}
45+
};
46+
const getParsedCookies = () =>
47+
Object.fromEntries(
48+
document.cookie.split('; ').map((cookie) => {
49+
const [key, ...value] = cookie.split('=');
50+
const decodedValue = decodeURIComponent(value.join('='));
51+
return [key, deserializer(decodedValue)];
52+
})
53+
);
54+
const [value, setValue] = useState(() => {
55+
if (typeof window === 'undefined') return {};
56+
return getParsedCookies();
57+
});
4858
useEffect(() => {
4959
const onChange = () => setValue(getParsedCookies());
5060
window.addEventListener(COOKIE_EVENT, onChange);
@@ -54,7 +64,7 @@ export const useCookies = () => {
5464
}, []);
5565
const set = (key, value, options) => {
5666
if (value === null) return removeCookieItem(key);
57-
setCookieItem(key, value, options);
67+
setCookieItem(key, serializer(value), options);
5868
};
5969
const remove = (key, options) => removeCookieItem(key, options);
6070
const getAll = () => getParsedCookies();

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

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,17 @@ const getStorageItem = (storage, key) => {
3838
* @example
3939
* const { value, set, remove } = useStorage('key', 'value');
4040
*/
41-
export const useStorage = (...params) => {
42-
const key = params[0];
43-
const secondParam = params[1];
41+
export const useStorage = (key, params) => {
4442
const options =
45-
typeof secondParam === 'object' &&
46-
secondParam &&
47-
('serializer' in secondParam ||
48-
'deserializer' in secondParam ||
49-
'initialValue' in secondParam ||
50-
'storage' in secondParam)
51-
? secondParam
43+
typeof params === 'object' &&
44+
params &&
45+
('serializer' in params ||
46+
'deserializer' in params ||
47+
'initialValue' in params ||
48+
'storage' in params)
49+
? params
5250
: undefined;
53-
const initialValue = options ? options?.initialValue : secondParam;
51+
const initialValue = options ? options?.initialValue : params;
5452
if (typeof window === 'undefined') {
5553
const value = typeof initialValue === 'function' ? initialValue() : initialValue;
5654
return {
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+
export const URL_SEARCH_PARAMS_EVENT = 'reactuse-url-search-params-event';
3+
export const getUrlSearchParams = (mode = 'history') => {
4+
const { search, hash } = window.location;
5+
let path = '';
6+
if (mode === 'history') path = search;
7+
if (mode === 'hash-params') path = hash.replace(/^#/, '');
8+
if (mode === 'hash') {
9+
const index = hash.indexOf('?');
10+
path = ~index ? hash.slice(index) : '';
11+
}
12+
return new URLSearchParams(path);
13+
};
14+
export const createQueryString = (searchParams, mode) => {
15+
const searchParamsString = searchParams.toString();
16+
const { search, hash } = window.location;
17+
if (mode === 'history') return `${searchParamsString ? `?${searchParamsString}` : ''}${hash}`;
18+
if (mode === 'hash-params')
19+
return `${search}${searchParamsString ? `#${searchParamsString}` : ''}`;
20+
if (mode === 'hash') {
21+
const index = hash.indexOf('?');
22+
const base = index > -1 ? hash.slice(0, index) : hash;
23+
return `${search}${base}${searchParamsString ? `?${searchParamsString}` : ''}`;
24+
}
25+
throw new Error('Invalid mode');
26+
};
27+
export const dispatchUrlSearchParamsEvent = () =>
28+
window.dispatchEvent(new Event(URL_SEARCH_PARAMS_EVENT));
29+
/**
30+
* @name useUrlSearchParam
31+
* @description - Hook that provides reactive URLSearchParams for a single key
32+
* @category Browser
33+
*
34+
* @overload
35+
* @template Value The type of the url param values
36+
* @param {string} key The key of the url param
37+
* @param {UseUrlSearchParamOptions<Value> & { initialValue: Value }} options The options object with required initialValue
38+
* @param {Value} options.initialValue The initial value for the url param
39+
* @param {UrlSearchParamsMode} [options.mode='history'] The mode to use for the URL ('history' | 'hash-params' | 'hash')
40+
* @param {'push' | 'replace'} [options.write='replace'] The mode to use for writing to the URL
41+
* @param {(value: Value) => string} [options.serializer] Custom serializer function to convert value to string
42+
* @param {(value: string) => Value} [options.deserializer] Custom deserializer function to convert string to value
43+
* @returns {UseUrlSearchParamReturn<Value>} The object with value and function for change value
44+
*
45+
* @example
46+
* const { value, set } = useUrlSearchParam('page', { initialValue: 1 });
47+
*
48+
* @overload
49+
* @template Value The type of the url param values
50+
* @param {string} key The key of the url param
51+
* @param {Value} [initialValue] The initial value for the url param
52+
* @returns {UseUrlSearchParamReturn<Value>} The object with value and function for change value
53+
*
54+
* @example
55+
* const { value, set } = useUrlSearchParam('page', 1);
56+
*/
57+
export const useUrlSearchParam = (key, params) => {
58+
const options =
59+
typeof params === 'object' &&
60+
params &&
61+
('serializer' in params ||
62+
'deserializer' in params ||
63+
'initialValue' in params ||
64+
'mode' in params ||
65+
'write' in params)
66+
? params
67+
: undefined;
68+
const initialValue = options ? options?.initialValue : params;
69+
const { mode = 'history', write: writeMode = 'replace' } = options ?? {};
70+
if (typeof window === 'undefined') {
71+
return {
72+
value: initialValue,
73+
remove: () => {},
74+
set: () => {}
75+
};
76+
}
77+
const serializer = (value) => {
78+
if (options?.serializer) return options.serializer(value);
79+
if (typeof value === 'string') return value;
80+
return JSON.stringify(value);
81+
};
82+
const deserializer = (value) => {
83+
if (options?.deserializer) return options.deserializer(value);
84+
if (value === 'undefined' || value === 'null') return undefined;
85+
try {
86+
return JSON.parse(value);
87+
} catch {
88+
return value;
89+
}
90+
};
91+
const setUrlSearchParam = (key, value, mode, write = 'replace') => {
92+
const searchParams = getUrlSearchParams(mode);
93+
const serializedValue =
94+
value !== undefined ? (serializer ? serializer(value) : String(value)) : '';
95+
if (value === undefined) {
96+
searchParams.delete(key);
97+
} else {
98+
searchParams.set(key, serializedValue);
99+
}
100+
const query = createQueryString(searchParams, mode);
101+
if (write === 'replace') window.history.replaceState({}, '', query);
102+
if (write === 'push') window.history.pushState({}, '', query);
103+
dispatchUrlSearchParamsEvent();
104+
};
105+
const [value, setValue] = useState(() => {
106+
const searchParams = getUrlSearchParams(mode);
107+
const currentValue = searchParams.get(key);
108+
if (currentValue === null && initialValue !== undefined) {
109+
setUrlSearchParam(key, initialValue, mode, writeMode);
110+
return initialValue;
111+
}
112+
return currentValue ? deserializer(currentValue) : undefined;
113+
});
114+
const set = (value, options) => {
115+
setUrlSearchParam(key, value, mode, options?.write ?? writeMode);
116+
setValue(value);
117+
};
118+
const remove = (options) => {
119+
setUrlSearchParam(key, undefined, mode, options?.write ?? writeMode);
120+
setValue(undefined);
121+
};
122+
useEffect(() => {
123+
const onParamsChange = () => {
124+
const searchParams = getUrlSearchParams(mode);
125+
const newValue = searchParams.get(key);
126+
setValue(newValue ? deserializer(newValue) : undefined);
127+
};
128+
window.addEventListener(URL_SEARCH_PARAMS_EVENT, onParamsChange);
129+
window.addEventListener('popstate', onParamsChange);
130+
if (mode !== 'history') {
131+
window.addEventListener('hashchange', onParamsChange);
132+
}
133+
return () => {
134+
window.removeEventListener(URL_SEARCH_PARAMS_EVENT, onParamsChange);
135+
window.removeEventListener('popstate', onParamsChange);
136+
if (mode !== 'history') {
137+
window.removeEventListener('hashchange', onParamsChange);
138+
}
139+
};
140+
}, [key, mode]);
141+
return {
142+
value,
143+
remove,
144+
set
145+
};
146+
};

0 commit comments

Comments
 (0)