Skip to content

Instantly share code, notes, and snippets.

@jokull
Last active November 21, 2022 09:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jokull/959d8c5dcdd7b10711666fb61177e778 to your computer and use it in GitHub Desktop.
Save jokull/959d8c5dcdd7b10711666fb61177e778 to your computer and use it in GitHub Desktop.
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable react-hooks/exhaustive-deps */
import { useRouter } from "next/router";
import { z } from "zod";
export function isZodType(
t: z.ZodTypeAny,
type: z.ZodFirstPartyTypeKind
): boolean {
if (t._def?.typeName === type) {
return true;
}
if (
t._def?.typeName === z.ZodFirstPartyTypeKind.ZodEffects &&
(t as z.ZodEffects<any>)._def.effect.type === "refinement"
) {
return isZodType((t as z.ZodEffects<any>).innerType(), type);
}
if (t._def?.innerType) {
return isZodType(t._def?.innerType, type);
}
return false;
}
export function withoutTransform(t: z.ZodTypeAny): z.ZodTypeAny {
if (t._def?.typeName === z.ZodFirstPartyTypeKind.ZodEffects) {
return withoutTransform((t as z.ZodEffects<any>).innerType());
}
return t;
}
function validateParam<T extends z.ZodDefault<z.ZodTypeAny>>(
schema: T,
parameter: string | null
): z.infer<T> {
let processed;
if (
(isZodType(schema, z.ZodFirstPartyTypeKind.ZodNumber) ||
isZodType(schema, z.ZodFirstPartyTypeKind.ZodBoolean)) &&
parameter &&
typeof parameter === "string"
) {
processed = z.preprocess<typeof schema>((x) => {
try {
return JSON.parse(x as string);
} catch {
return x;
}
}, schema);
} else {
processed = schema;
}
try {
const parsed: z.infer<T> = processed.parse(parameter);
return parsed;
} catch (error) {
return schema._def.defaultValue();
}
}
function useSearchParam<T extends z.ZodDefault<z.ZodTypeAny>>(
key: string,
schema: T
): [z.infer<T>, (newValue: string | z.infer<T>) => void] {
const router = useRouter();
let searchParams: URLSearchParams;
if (router.isReady && router.asPath.includes("?")) {
searchParams = new URLSearchParams(router.asPath.split("?", 2).at(1));
} else {
searchParams = new URLSearchParams();
}
console.log({ searchParams: searchParams.toString() });
const value = validateParam(
schema,
searchParams.get(key) ?? schema._def.defaultValue()
);
function setValue(newValue: string | z.infer<T>) {
const newSearchParams = new URLSearchParams(searchParams);
const validated = validateParam(schema, newValue);
const stringified =
isZodType(schema, z.ZodFirstPartyTypeKind.ZodNumber) ||
isZodType(schema, z.ZodFirstPartyTypeKind.ZodBoolean)
? JSON.stringify(validated)
: newValue;
console.log({
newValue,
newSearchParams: newSearchParams.toString(),
validated,
stringified,
defaultValue: schema._def.defaultValue(),
isEqualDefault: newValue === schema._def.defaultValue(),
});
if (newValue === schema._def.defaultValue()) {
newSearchParams.delete(key);
} else {
newSearchParams.set(key, stringified);
}
void router.push(`${router.pathname}?${newSearchParams.toString()}`);
}
return [value, setValue];
}
export default function Page() {
const [name, setName] = useSearchParam("name", z.string().default(""));
const [id, setId] = useSearchParam("id", z.number().default(1));
const [nullableId, setNullableId] = useSearchParam(
"nullableId",
z.number().nullable().default(1)
);
const [mobile, setMobile] = useSearchParam(
"mobile",
z.boolean().default(true)
);
return (
<div className="m-12 mx-auto max-w-md">
<div className="mb-8 flex flex-col gap-4">
<div>
<strong>name</strong>: {name} is {typeof name}
</div>
<div>
<strong>id</strong>: {id} is {typeof id}
</div>
<div>
<strong>mobile</strong>: {String(mobile)} is {typeof mobile}
</div>
<div>
<strong>nullableId</strong>: {String(nullableId)} is{" "}
{typeof nullableId}
</div>
</div>
<div className="flex flex-col items-start gap-4">
<input
value={name}
onChange={({ target }) => {
setName(target.value);
}}
className="rounded-lg border px-2 py-1"
/>
<button
className="rounded-lg border px-2 py-1"
onClick={() => {
setId(id + 1);
}}
>
Increment id
</button>
<div className="flex gap-4">
<button
className="rounded-lg border px-2 py-1"
onClick={() => {
setNullableId(
typeof nullableId === "number" ? nullableId + 1 : 1
);
}}
>
Increment nullable id
</button>
<button
className="rounded-lg border px-2 py-1"
onClick={() => {
setNullableId(null);
}}
>
Nullify nullableId
</button>
</div>
<button
className="rounded-lg border px-2 py-1"
onClick={() => {
setMobile(!mobile);
}}
>
Flip
</button>
</div>
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment