-
-
Save codemem/5b03402ee43b1cc3a2f6892a8cbef06e to your computer and use it in GitHub Desktop.
Revisions
-
Temzasse revised this gist
Apr 2, 2024 . 1 changed file with 29 additions and 12 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -3,11 +3,27 @@ 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(); @@ -41,28 +57,29 @@ export function useParsedSearchParams<T extends ParseConfig>(config: T) { 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 }, -
Temzasse created this gist
Apr 2, 2024 .There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,73 @@ import { useMemo } from "react"; import { useSearchParams } from "react-router-dom"; type ParseConfig = Record< string, | { type: "string"; defaultValue: string | undefined } | { type: "number"; defaultValue: number | undefined } | { parse: (value: URLSearchParams) => any } >; 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: "number" | "string"; defaultValue: any } ? // Handle the case where the `defaultValue` is `undefined` undefined extends T[K]["defaultValue"] ? T[K]["type"] extends "number" ? number | undefined : string | undefined : // Get the type base on the `type` key T[K]["type"] extends "number" ? number : string : never; }; return { parsedParams, setSearchParams }; // The `config` object is not expected to change during the component lifecycle }, [searchParams]); // eslint-disable-line react-hooks/exhaustive-deps } // Usage example 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)) }, });