Created
July 27, 2022 11:19
-
-
Save C-Sinclair/fe4d1db8e6c44912424ea6f04708d303 to your computer and use it in GitHub Desktop.
Zod typesafe localstorage hook
This file contains 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 characters
import { useCallback, useState } from "react"; | |
import { z, ZodType } from "zod"; | |
type Value<S extends ZodType> = z.infer<S>; | |
type ValueSetter<S extends ZodType> = (currentValue: Value<S>) => void; | |
type UseZodls<S extends ZodType> = [ | |
/** | |
* The current value of the data from local storage | |
* Will default to the defaultValue if not found in local storage | |
*/ | |
value: Value<S>, | |
/** | |
* Updates localStorage with the new value | |
* @throws if the provided value does not pass the type check | |
*/ | |
set: (value: Value<S> | ValueSetter<S>) => void | |
]; | |
/** | |
* React hook enabling Typesafe access to localStorage | |
* | |
* @param key {string} -- the localStorage string key of where to find the data | |
* @param schema -- the Zod schema to ensure the data matches | |
* @param defaultValue -- the default object to return on error or missing data | |
* | |
* @example | |
* ```tsx | |
* const schema = z.object({ | |
* name: z.string(), | |
* }) | |
* const [value, setValue] = useZodls("keytostore", schema, { name: "default" }) | |
* value // { name: "default" } | |
* setValue({ name: "newName" }) | |
* value // { name: "newName" } | |
* ``` | |
*/ | |
export function useZodls<S extends ZodType>( | |
key: string, | |
schema: S, | |
defaultValue: Value<S> | |
): UseZodls<S> { | |
const get = useCallback(() => { | |
try { | |
const stored = JSON.parse(localStorage.getItem(key)); | |
const parsed = schema.safeParse(stored); | |
if (parsed.success) { | |
return parsed.data; | |
} | |
return defaultValue; | |
} catch (e) { | |
// if the JSON.parse fails we still want to return the defaultValue | |
return defaultValue; | |
} | |
}, [key, schema, defaultValue]); | |
const [value, setValue] = useState<Value<S> | undefined>(get); | |
const set = useCallback( | |
(value: Value<S> | ValueSetter<S>) => { | |
let newValue: Value<S> = value; | |
if (typeof value === "function") { | |
const valueSetter = value as ValueSetter<S>; | |
const current = get(); | |
newValue = valueSetter(current); | |
} | |
// might throw | |
schema.parse(newValue); | |
localStorage.setItem(key, JSON.stringify(newValue)); | |
setValue(newValue); | |
}, | |
// eslint-disable-next-line react-hooks/exhaustive-deps -- ESlint wants `S` to be a dependency, but its a type?!! | |
[setValue, get, schema, key] | |
); | |
return [value, set]; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment