Search code examples
typescriptlocal-storagesolid-js

How to update local storage values in SolidJS using hooks


I'm trying to make a custom "hook" for solid-js that will retrieve state from local storage.

import { Accessor, createSignal, Setter } from "solid-js";

export default function createLocalStorageSignal<T extends string>(key: string): [get: Accessor<T>, set: Setter<T>] {
    const storage = window.localStorage;
    const initialValue: T = JSON.parse(storage.getItem(key) ?? '{}').value;

    const [value,setValue] = createSignal<T>(initialValue);

    const newSetValue: Setter<T> = (newValue) => {
            setValue(newValue);
            storage.setItem(key, JSON.stringify({value: newValue}));

            return newValue;
        }

    return [
        value,
        newSetValue
    ]
}

However I get the type error

Type '(newValue: any) => void' is not assignable to type 'Setter<T>'

Why can't newValue's type be inferred? and if it can't be inferred what do I set it to?

EDIT: Setter<T>'s full type is

type Setter<T> = undefined extends T ? 
    <U extends T>
        (v?: (U extends Function ? never : U) |
        ((prev?: T | undefined) => U) | undefined) => U : 
    <U extends T>
        (v: (U extends Function ? never : U) |
        ((prev: T) => U)) => U

I don't exactly understand the purpose of the U type and how it works. I think the problem relates to the fact that newValue could potentially be a function but the T type could also be a function type or something...


Solution

  • Here's one way to do it:

    import { Accessor, createSignal, Setter } from "solid-js";
    
    export default function createLocalStorageSignal<T extends object>(
      key: string
    ): T extends (...args: never) => unknown ? unknown : [get: Accessor<T>, set: Setter<T>];
    export default function createLocalStorageSignal<T extends object>(key: string): [Accessor<T>, Setter<T>] {
      const storage = window.localStorage;
      const initialValue: T = JSON.parse(storage.getItem(key) ?? "{}").value;
    
      const [value, setValue] = createSignal<T>(initialValue);
    
      const newSetValue = (newValue: T | ((v: T) => T)): T => {
        const _val: T = typeof newValue === 'function' ? newValue(value()) : newValue
    
        setValue(_val as any);
        storage.setItem(key, JSON.stringify({ value: _val }));
    
        return _val;
      };
    
      return [value, newSetValue as Setter<T>];
    }
    
    type MyObjectType = {
      foo: string
      bar: number
    }
    
    const [get, set] = createLocalStorageSignal<MyObjectType>('asdf')
    
    const val = get() // type of val is MyObjectType
    
    set({} as MyObjectType) // ok
    set(() => ({} as MyObjectType)) // ok
    set((prev: MyObjectType) => ({} as MyObjectType)) // ok
    
    const str: string = val.foo // ok
    const num: number = val.bar // ok
    
    const bool: boolean = val.foo // string is not assignable to boolean (as expected)
    const sym: symbol = val.bar // number is not assignable to symbol (as expected)
    
    
    // This is made to have a type error because function values can not be JSON.stringified.
    const [get2, set2] = createLocalStorageSignal<() => void>('asdf')
    
    const val2 = get2() // type of val is any, but that's because of the previous error.
    

    TS playground example