-
-
Save codemem/5b03402ee43b1cc3a2f6892a8cbef06e to your computer and use it in GitHub Desktop.
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.
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 { 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<T extends ParseConfig>(config: T) { | |
const [searchParams, setSearchParams] = useSearchParams(); | |
return useMemo(() => { | |
const parsed: Record<string, any> = {}; | |
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)) }, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment