import { useMemo } from "react"; import { useSearchParams } from "react-router-dom"; type ParseConfig = Record< string, | { type: "string"; defaultValue?: string } | { type: "number"; defaultValue?: number } | { parse: (value: URLSearchParams) => unknown } >; /** * A utility hook to parse and type URL search params based on a configuration * object. This hook is useful when you want to access URL search params in a * typesafe way and with proper casting. * * @example * ```tsx * const { parsedParams } = useParsedSearchParams({ * page: { type: "number", defaultValue: 1 }, * search: { type: "string", defaultValue: "" }, * order: { type: "string", defaultValue: "asc" }, * sort: { type: "string" }, // You can omit default value * selected: { parse: (p) => new Set(p.getAll("selected").map(Number)) }, * }); * ``` */ export function useParsedSearchParams(config: T) { const [searchParams, setSearchParams] = useSearchParams(); return useMemo(() => { const parsed: Record = {}; for (const [key, options] of Object.entries(config)) { if ("parse" in options) { parsed[key] = options.parse(searchParams); continue; } const value = searchParams.get(key); const { type, defaultValue } = options; if (value !== null) { if (type === "number") { const numValue = Number(value); parsed[key] = isNaN(numValue) ? defaultValue : numValue; } else { parsed[key] = value; } } else { parsed[key] = defaultValue; } } // Typing this without casting is impossible... const parsedParams = parsed as { [K in keyof T]: T[K] extends { parse: (value: URLSearchParams) => infer P; } ? P : T[K] extends { type: infer TType extends "number" | "string"; defaultValue?: infer TDefault; } ? // Handle the case where the `defaultValue` is `undefined` undefined extends TDefault ? TType extends "number" ? number | undefined : string | undefined : // Get the type based on the `defaultValue` type TDefault : never; }; return { parsedParams, setSearchParams, }; // The `config` object is not expected to change during the component lifecycle }, [searchParams]); // eslint-disable-line react-hooks/exhaustive-deps } // Test it out const { parsedParams } = useParsedSearchParams({ page: { type: "number", defaultValue: 1 }, search: { type: "string", defaultValue: "" }, sort: { type: "string", defaultValue: undefined }, order: { type: "string", defaultValue: "asc" }, selected: { parse: (p) => new Set(p.getAll("selected").map(Number)) }, });