|
| 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