Skip to content

Instantly share code, notes, and snippets.

@ricosandyca
Last active January 23, 2024 04:28
Show Gist options
  • Save ricosandyca/3a994b10388117befb1911dac703aad2 to your computer and use it in GitHub Desktop.
Save ricosandyca/3a994b10388117befb1911dac703aad2 to your computer and use it in GitHub Desktop.
React Hook - Query State
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