refactor(helpers): modernize reactive helpers and add tests
This commit is contained in:
@@ -1,10 +1,66 @@
|
||||
/**
|
||||
* Reusable persistent storage utility using Svelte 5 runes
|
||||
* Persistent localStorage-backed reactive state
|
||||
*
|
||||
* Automatically syncs state with localStorage.
|
||||
* Creates reactive state that automatically syncs with localStorage.
|
||||
* Values persist across browser sessions and are restored on page load.
|
||||
*
|
||||
* Handles edge cases:
|
||||
* - SSR safety (no localStorage on server)
|
||||
* - JSON parse errors (falls back to default)
|
||||
* - Storage quota errors (logs warning, doesn't crash)
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Store user preferences
|
||||
* const preferences = createPersistentStore('user-prefs', {
|
||||
* theme: 'dark',
|
||||
* fontSize: 16,
|
||||
* sidebarOpen: true
|
||||
* });
|
||||
*
|
||||
* // Access reactive state
|
||||
* $: currentTheme = preferences.value.theme;
|
||||
*
|
||||
* // Update (auto-saves to localStorage)
|
||||
* preferences.value.theme = 'light';
|
||||
*
|
||||
* // Clear stored value
|
||||
* preferences.clear();
|
||||
* ```
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a reactive store backed by localStorage
|
||||
*
|
||||
* The value is loaded from localStorage on initialization and automatically
|
||||
* saved whenever it changes. Uses Svelte 5's $effect for reactive sync.
|
||||
*
|
||||
* @param key - localStorage key for storing the value
|
||||
* @param defaultValue - Default value if no stored value exists
|
||||
* @returns Persistent store with getter/setter and clear method
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Simple value
|
||||
* const counter = createPersistentStore('counter', 0);
|
||||
* counter.value++;
|
||||
*
|
||||
* // Complex object
|
||||
* interface Settings {
|
||||
* theme: 'light' | 'dark';
|
||||
* fontSize: number;
|
||||
* }
|
||||
* const settings = createPersistentStore<Settings>('app-settings', {
|
||||
* theme: 'light',
|
||||
* fontSize: 16
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function createPersistentStore<T>(key: string, defaultValue: T) {
|
||||
// Initialize from storage or default
|
||||
/**
|
||||
* Load value from localStorage or return default
|
||||
* Safely handles missing keys, parse errors, and SSR
|
||||
*/
|
||||
const loadFromStorage = (): T => {
|
||||
if (typeof window === 'undefined') {
|
||||
return defaultValue;
|
||||
@@ -21,6 +77,7 @@ export function createPersistentStore<T>(key: string, defaultValue: T) {
|
||||
let value = $state<T>(loadFromStorage());
|
||||
|
||||
// Sync to storage whenever value changes
|
||||
// Wrapped in $effect.root to prevent memory leaks
|
||||
$effect.root(() => {
|
||||
$effect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
@@ -29,18 +86,27 @@ export function createPersistentStore<T>(key: string, defaultValue: T) {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
} catch (error) {
|
||||
// Quota exceeded or privacy mode - log but don't crash
|
||||
console.warn(`[createPersistentStore] Error saving ${key}:`, error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
/**
|
||||
* Current value (getter/setter)
|
||||
* Changes automatically persist to localStorage
|
||||
*/
|
||||
get value() {
|
||||
return value;
|
||||
},
|
||||
set value(v: T) {
|
||||
value = v;
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove value from localStorage and reset to default
|
||||
*/
|
||||
clear() {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem(key);
|
||||
@@ -50,4 +116,7 @@ export function createPersistentStore<T>(key: string, defaultValue: T) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Type representing a persistent store instance
|
||||
*/
|
||||
export type PersistentStore<T> = ReturnType<typeof createPersistentStore<T>>;
|
||||
|
||||
Reference in New Issue
Block a user