import { batch, createEffect, createSignal, onMount } from "solid-js"; import { createStore, reconcile } from "solid-js/store"; export const createLocalStore = function >( initState: T, { prefix = "app", serializer = (v: any) => JSON.stringify(v), deserializer = (v: any) => JSON.parse(v), migrate = undefined as | ((getState: () => Record) => boolean) | undefined, } = {} ) { const [state, setState] = createStore(initState); const [mounted, setMounted] = createSignal(false); const localStorage = globalThis.localStorage; const storePrefix = `${prefix}-`; if (localStorage) { let mounts = 0; let mounted_ = false; const updating = {} as Record; const keys = Object.keys(state); const changedBeforeMount = {} as Record; if (migrate) { let migratedState: Record | undefined = undefined; const getMigrateState = () => { if (migratedState) return migratedState; migratedState = {}; for (const key of Object.keys(localStorage)) { if (!key.startsWith(storePrefix)) { continue; } migratedState[key.slice(storePrefix.length)] = deserializer( localStorage.getItem(key) ); } return migratedState; }; const migrated = migrate(getMigrateState); if (migrated && migratedState != undefined) { for (const [key, value] of Object.entries(migratedState)) { localStorage.setItem(`${storePrefix}${key}`, serializer(value)); } } migratedState = undefined; } for (const key of keys) { let storeKey = `${storePrefix}${key}`; let mountValue = localStorage.getItem(storeKey); let initRun = true; const [updatingCount, setUpdatingCount] = createSignal(0); // TODO: Implement localStorage listener createEffect(() => { // During mounts we want to always run this effect even if the state value hasnt changed // We need to run it always to reset updating[key] updatingCount(); const isInitRun = initRun; initRun = false; const value = serializer(state[key]); if (isInitRun && mountValue) { return; } // If the key is getting mounted at the moment, we skip the localStorage set if (updating[key]) { updating[key] = false; return; } if (!mounted_) { changedBeforeMount[key] = true; } if (value === undefined) { localStorage.removeItem(storeKey); } else { localStorage.setItem(storeKey, value); } }); onMount(() => { if (!changedBeforeMount[key] && mountValue) { updating[key] = true; batch(function () { setUpdatingCount(updatingCount() + 1); setState(key as any, reconcile(deserializer(mountValue))); }); } mounts++; mountValue = null; if (mounts === keys.length) { setMounted(true); mounted_ = true; } }); } } return [state, setState, mounted] as [ typeof state, typeof setState, typeof mounted ]; };