|
1 | 1 | import { useCallback, useState } from 'react'; |
| 2 | +import { z } from 'zod'; |
| 3 | +import { Kit } from '../kit'; |
2 | 4 |
|
3 | | -export function useLocalStorageJson<T>(key: string, defaultValue: T) { |
4 | | - const [value, setValue] = useState<T>(() => { |
5 | | - const json = localStorage.getItem(key); |
| 5 | +export function useLocalStorageJson<$Schema extends z.ZodType>(...args: ArgsInput<$Schema>) { |
| 6 | + const [key, schema, manualDefaultValue] = args as any as Args<$Schema>; |
| 7 | + // The parameter types will force the user to give a manual default |
| 8 | + // if their given Zod schema does not have default. |
| 9 | + // |
| 10 | + // We resolve that here because in the event of a Zod parse failure, we fallback |
| 11 | + // to the default value, meaning we are needing a reference to the Zod default outside |
| 12 | + // of the regular parse process. |
| 13 | + // |
| 14 | + const defaultValue = |
| 15 | + manualDefaultValue !== undefined |
| 16 | + ? manualDefaultValue |
| 17 | + : Kit.ZodHelpers.isDefaultType(schema) |
| 18 | + ? (schema._def.defaultValue() as z.infer<$Schema>) |
| 19 | + : Kit.never(); |
| 20 | + |
| 21 | + const [value, setValue] = useState<z.infer<$Schema>>(() => { |
| 22 | + // Note: `null` is returned for missing values. However Zod only kicks in |
| 23 | + // default values for `undefined`, not `null`. However-however, this is ok, |
| 24 | + // because we manually pre-compute+return the default value, thus we don't |
| 25 | + // rely on Zod's behaviour. If that changes this should have `?? undefined` |
| 26 | + // added. |
| 27 | + const storedValue = localStorage.getItem(key); |
| 28 | + |
| 29 | + if (!storedValue) { |
| 30 | + return defaultValue; |
| 31 | + } |
| 32 | + |
| 33 | + // todo: Some possible improvements: |
| 34 | + // - Monitor json/schema parse failures. |
| 35 | + // - Let caller choose an error strategy: 'return' / 'default' / 'throw' |
6 | 36 | try { |
7 | | - const result = json ? JSON.parse(json) : defaultValue; |
8 | | - return result; |
9 | | - } catch (_) { |
| 37 | + return schema.parse(JSON.parse(storedValue)); |
| 38 | + } catch (error) { |
| 39 | + if (error instanceof SyntaxError) { |
| 40 | + console.warn(`useLocalStorageJson: JSON parsing failed for key "${key}"`, error); |
| 41 | + } else if (error instanceof z.ZodError) { |
| 42 | + console.warn(`useLocalStorageJson: Schema validation failed for key "${key}"`, error); |
| 43 | + } else { |
| 44 | + Kit.neverCatch(error); |
| 45 | + } |
10 | 46 | return defaultValue; |
11 | 47 | } |
12 | 48 | }); |
13 | 49 |
|
14 | 50 | const set = useCallback( |
15 | | - (value: T) => { |
| 51 | + (value: z.infer<$Schema>) => { |
16 | 52 | localStorage.setItem(key, JSON.stringify(value)); |
17 | 53 | setValue(value); |
18 | 54 | }, |
19 | | - [setValue], |
| 55 | + [key], |
20 | 56 | ); |
21 | 57 |
|
22 | 58 | return [value, set] as const; |
23 | 59 | } |
| 60 | + |
| 61 | +type ArgsInput<$Schema extends z.ZodType> = |
| 62 | + $Schema extends z.ZodDefault<z.ZodType> |
| 63 | + ? [key: string, schema: ArgsInputGuardZodJsonSchema<$Schema>] |
| 64 | + : [key: string, schema: ArgsInputGuardZodJsonSchema<$Schema>, defaultValue: z.infer<$Schema>]; |
| 65 | + |
| 66 | +type ArgsInputGuardZodJsonSchema<$Schema extends z.ZodType> = |
| 67 | + z.infer<$Schema> extends Kit.Json.Value |
| 68 | + ? $Schema |
| 69 | + : 'Error: Your Zod schema is or contains a type that is not valid JSON.'; |
| 70 | + |
| 71 | +type Args<$Schema extends z.ZodType> = [ |
| 72 | + key: string, |
| 73 | + schema: $Schema, |
| 74 | + defaultValue?: z.infer<$Schema>, |
| 75 | +]; |
0 commit comments