Skip to content

Instantly share code, notes, and snippets.

@Temzasse
Last active April 6, 2024 10:12
Show Gist options
  • Save Temzasse/04cce026035116143c60afd3882e8f56 to your computer and use it in GitHub Desktop.
Save Temzasse/04cce026035116143c60afd3882e8f56 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.
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