Last active
January 23, 2024 04:28
-
-
Save ricosandyca/3a994b10388117befb1911dac703aad2 to your computer and use it in GitHub Desktop.
React Hook - Query State
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useCallback, useMemo } from 'react'; | |
import { useSearchParams } from 'react-router-dom'; | |
// helper types | |
export type Nullable<T> = T | undefined | null; | |
export type NullableObject<T extends object> = { [K in keyof T]?: T[K] | null }; | |
export type QueryStateValue = string | number | boolean; | |
export type UseQueryStateSetterFunctionType<T = any> = (prev: T) => T; | |
export type UseQueryStateSetterType<T = any> = | |
| T | |
| UseQueryStateSetterFunctionType<T>; | |
// null or undefined type checking | |
// using typescript type predicates | |
// @see: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates | |
export const isNil = (value: any): value is null | undefined => { | |
return value === undefined || value === null; | |
}; | |
export const parseValue: <T = any>(val: string) => T = (val) => { | |
// parse boolean | |
if (val === 'false' || val === 'true') return val === 'true'; | |
// parse integer | |
if (!isNaN(+val)) return val; | |
// parse json | |
if (isValidJson(val)) return JSON.parse(val); | |
return val as any; | |
}; | |
/** | |
* Controlled state between query string | |
* when state changes to undefined or null, the value will be reset to default | |
* | |
* @param key - query string key | |
* @param defaultValue - default value if the qs value of key doesn't exist | |
* @returns controlled state like react state's way | |
*/ | |
export function useQueryState<T extends QueryStateValue>( | |
key: string, | |
defaultValue: T, | |
): [T, (val: UseQueryStateSetterType<Nullable<T>>) => void] { | |
const [params, setParams] = useSearchParams(); | |
const value = useMemo<T>(() => { | |
const val = params.get(key); | |
if (!val) return defaultValue; | |
return parseValue(val) as T; | |
}, [key, defaultValue, params]); | |
const setValue = useCallback( | |
(arg: UseQueryStateSetterType<Nullable<T>>) => { | |
if (typeof arg === 'function') arg = arg(value); | |
if (isNil(arg)) params.delete(key); | |
else params.set(key, arg.toString()); | |
setParams(params); | |
}, | |
[params, setParams, key, value], | |
); | |
return [value, setValue]; | |
} | |
/** | |
* Controlled multiple states between query string | |
* this works just like regular `useQueryState` | |
* but this allows to change multiple qs at a time | |
* | |
* @param defaultValues - object of default value with pattern {[key]: value} | |
* @returns controlled state like react state's way | |
*/ | |
export function useMultiQueryState<T extends Record<string, QueryStateValue>>( | |
defaultValues: T, | |
): [T, (val: UseQueryStateSetterType<NullableObject<T>>) => void] { | |
const [params, setParams] = useSearchParams(); | |
const values = useMemo<T>(() => { | |
const values = defaultValues; | |
for (const key in values) { | |
const val = params.get(key); | |
if (!val) continue; | |
const parsedValue = parseValue(val); | |
values[key] = parsedValue; | |
} | |
return values; | |
}, [JSON.stringify(defaultValues), params]); | |
const setValues = useCallback( | |
(arg: UseQueryStateSetterType<NullableObject<T>>) => { | |
if (typeof arg === 'function') arg = arg(values); | |
for (const key in arg) { | |
const argVal = arg[key]; | |
if (isNil(argVal)) params.delete(key); | |
else params.set(key, argVal.toString()); | |
} | |
setParams(params); | |
}, | |
[params, setParams, values], | |
); | |
return [values, setValues]; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment