From 2cddad4336cca500c6bbec08409938e9cdd81ddd Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Mon, 19 Dec 2022 22:34:46 +0000 Subject: [PATCH] Typed query hook (#6034) * Typed query hook * Add optional keys * safeParse query params * Add use callback * Fix re-renders * Add schemas to support query params - fix keys * Update packages/lib/hooks/useTypedQuery.ts * Fix submodules * Fix website submodule * Reset website submodule * Merry Xmas Sean * Whoops Co-authored-by: zomars Co-authored-by: Peer Richelsen Co-authored-by: Jeroen Reumkens Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- packages/lib/hooks/useTypedQuery.ts | 88 +++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 packages/lib/hooks/useTypedQuery.ts diff --git a/packages/lib/hooks/useTypedQuery.ts b/packages/lib/hooks/useTypedQuery.ts new file mode 100644 index 0000000000..97246781fc --- /dev/null +++ b/packages/lib/hooks/useTypedQuery.ts @@ -0,0 +1,88 @@ +import { useRouter } from "next/router"; +import { useCallback, useMemo } from "react"; +import { z } from "zod"; + +type OptionalKeys = { [K in keyof T]-?: Record extends Pick ? K : never }[keyof T]; + +type FilteredKeys = { [K in keyof T as T[K] extends U ? K : never]: T[K] }; + +// Take array as a string and return zod array +export const queryNumberArray = z + .string() + .or(z.number()) + .or(z.array(z.number())) + .transform((a) => { + if (typeof a === "string") return a.split(",").map((a) => Number(a)); + if (Array.isArray(a)) return a; + return [a]; + }); + +// Take array as a string and return zod number array + +// Take string and return return zod string array - comma separated +export const queryStringArray = z + .preprocess((a) => z.string().parse(a).split(","), z.string().array()) + .or(z.string().array()); + +export function useTypedQuery(schema: T) { + type Output = z.infer; + type FullOutput = Required; + type OutputKeys = Required; + type OutputOptionalKeys = OptionalKeys; + type ArrayOutput = FilteredKeys>; + type ArrayOutputKeys = keyof ArrayOutput; + + const { query: unparsedQuery, ...router } = useRouter(); + const parsedQuerySchema = schema.safeParse(unparsedQuery); + + let parsedQuery: Output = useMemo(() => { + return {} as Output; + }, []); + + if (parsedQuerySchema.success) parsedQuery = parsedQuerySchema.data; + else if (!parsedQuerySchema.success) console.error(parsedQuerySchema.error); + + // Set the query based on schema values + const setQuery = useCallback( + function setQuery(key: J, value: Output[J]) { + // Remove old value by key so we can merge new value + const { [key]: _, ...newQuery } = parsedQuery; + const newValue = { ...newQuery, [key]: value }; + const search = new URLSearchParams(newValue as any).toString(); + router.replace({ query: search }, undefined, { shallow: true }); + }, + [parsedQuery, router] + ); + + // Delete a key from the query + function removeByKey(key: OutputOptionalKeys) { + const { [key]: _, ...newQuery } = parsedQuery; + router.replace({ query: newQuery as Output }, undefined, { shallow: true }); + } + + // push item to existing key + function pushItemToKey(key: J, value: ArrayOutput[J][number]) { + const existingValue = parsedQuery[key]; + if (Array.isArray(existingValue)) { + if (existingValue.includes(value)) return; // prevent adding the same value to the array + setQuery(key, [...existingValue, value]); + } else { + setQuery(key, [value]); + } + } + + // Remove item by key and value + function removeItemByKeyAndValue(key: J, value: ArrayOutput[J][number]) { + const existingValue = parsedQuery[key]; + if (Array.isArray(existingValue)) { + // @ts-expect-error this is too much for TS it seems + const newValue = existingValue.filter((item) => item !== value); + setQuery(key, newValue); + } else { + // @ts-expect-error this is too much for TS it seems + removeByKey(key); + } + } + + return { data: parsedQuery, setQuery, removeByKey, pushItemToKey, removeItemByKeyAndValue }; +}