Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save codemem/5b03402ee43b1cc3a2f6892a8cbef06e to your computer and use it in GitHub Desktop.

Select an option

Save codemem/5b03402ee43b1cc3a2f6892a8cbef06e to your computer and use it in GitHub Desktop.

Revisions

  1. @Temzasse Temzasse revised this gist Apr 2, 2024. 1 changed file with 29 additions and 12 deletions.
    41 changes: 29 additions & 12 deletions useParsedSearchParams.tsx
    Original 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 | undefined }
    | { type: "number"; defaultValue: number | undefined }
    | { parse: (value: URLSearchParams) => any }
    | { 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: "number" | "string"; defaultValue: any }
    : T[K] extends {
    type: infer TType extends "number" | "string";
    defaultValue?: infer TDefault;
    }
    ? // Handle the case where the `defaultValue` is `undefined`
    undefined extends T[K]["defaultValue"]
    ? T[K]["type"] extends "number"
    undefined extends TDefault
    ? TType extends "number"
    ? number | undefined
    : string | undefined
    : // Get the type base on the `type` key
    T[K]["type"] extends "number"
    ? number
    : string
    : // Get the type based on the `defaultValue` type
    TDefault
    : never;
    };

    return {
    parsedParams,
    setSearchParams
    setSearchParams,
    };
    // The `config` object is not expected to change during the component lifecycle
    }, [searchParams]); // eslint-disable-line react-hooks/exhaustive-deps
    }

    // Usage example
    // Test it out

    const { parsedParams } = useParsedSearchParams({
    page: { type: "number", defaultValue: 1 },
  2. @Temzasse Temzasse created this gist Apr 2, 2024.
    73 changes: 73 additions & 0 deletions useParsedSearchParams.tsx
    Original 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)) },
    });