@usefy/use-local-storage 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +62 -35
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +63 -36
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -71,6 +71,18 @@ function useLocalStorage(key, initialValue, options = {}) {
|
|
|
71
71
|
const cacheRef = (0, import_react.useRef)(
|
|
72
72
|
null
|
|
73
73
|
);
|
|
74
|
+
const readErrorRef = (0, import_react.useRef)(
|
|
75
|
+
null
|
|
76
|
+
);
|
|
77
|
+
const serverSnapshotRef = (0, import_react.useRef)(null);
|
|
78
|
+
const readServerSnapshot = (0, import_react.useCallback)(() => {
|
|
79
|
+
if (!serverSnapshotRef.current) {
|
|
80
|
+
serverSnapshotRef.current = {
|
|
81
|
+
value: resolveInitialValue(initialValueRef.current)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return serverSnapshotRef.current.value;
|
|
85
|
+
}, []);
|
|
74
86
|
const isClient = typeof window !== "undefined";
|
|
75
87
|
const subscribeToStore = (0, import_react.useCallback)(
|
|
76
88
|
(onStoreChange) => {
|
|
@@ -78,7 +90,7 @@ function useLocalStorage(key, initialValue, options = {}) {
|
|
|
78
90
|
let handleStorageEvent = null;
|
|
79
91
|
if (isClient && syncTabs) {
|
|
80
92
|
handleStorageEvent = (event) => {
|
|
81
|
-
if (event.key === key) {
|
|
93
|
+
if (event.key === key || event.key === null) {
|
|
82
94
|
onStoreChange();
|
|
83
95
|
}
|
|
84
96
|
};
|
|
@@ -95,60 +107,75 @@ function useLocalStorage(key, initialValue, options = {}) {
|
|
|
95
107
|
);
|
|
96
108
|
const getSnapshot = (0, import_react.useCallback)(() => {
|
|
97
109
|
if (!isClient) {
|
|
98
|
-
return
|
|
110
|
+
return readServerSnapshot();
|
|
99
111
|
}
|
|
112
|
+
let rawValue;
|
|
100
113
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
114
|
+
rawValue = window.localStorage.getItem(key);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (cacheRef.current && cacheRef.current.rawValue === null) {
|
|
103
117
|
return cacheRef.current.parsedValue;
|
|
104
118
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
119
|
+
const fallbackValue = resolveInitialValue(initialValueRef.current);
|
|
120
|
+
cacheRef.current = { rawValue: null, parsedValue: fallbackValue };
|
|
121
|
+
readErrorRef.current = { rawValue: null, error };
|
|
122
|
+
return fallbackValue;
|
|
123
|
+
}
|
|
124
|
+
if (cacheRef.current && cacheRef.current.rawValue === rawValue) {
|
|
125
|
+
return cacheRef.current.parsedValue;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const parsedValue = rawValue !== null ? deserializerRef.current(rawValue) : resolveInitialValue(initialValueRef.current);
|
|
111
129
|
cacheRef.current = { rawValue, parsedValue };
|
|
112
130
|
return parsedValue;
|
|
113
131
|
} catch (error) {
|
|
114
|
-
onErrorRef.current?.(error);
|
|
115
132
|
const fallbackValue = resolveInitialValue(initialValueRef.current);
|
|
116
|
-
cacheRef.current = { rawValue
|
|
133
|
+
cacheRef.current = { rawValue, parsedValue: fallbackValue };
|
|
134
|
+
readErrorRef.current = { rawValue, error };
|
|
117
135
|
return fallbackValue;
|
|
118
136
|
}
|
|
119
|
-
}, [key, isClient]);
|
|
120
|
-
const getServerSnapshot =
|
|
121
|
-
return resolveInitialValue(initialValueRef.current);
|
|
122
|
-
}, []);
|
|
137
|
+
}, [key, isClient, readServerSnapshot]);
|
|
138
|
+
const getServerSnapshot = readServerSnapshot;
|
|
123
139
|
const storedValue = (0, import_react.useSyncExternalStore)(
|
|
124
140
|
subscribeToStore,
|
|
125
141
|
getSnapshot,
|
|
126
142
|
getServerSnapshot
|
|
127
143
|
);
|
|
144
|
+
const reportedErrorRawRef = (0, import_react.useRef)(void 0);
|
|
145
|
+
(0, import_react.useEffect)(() => {
|
|
146
|
+
const pending = readErrorRef.current;
|
|
147
|
+
if (pending && reportedErrorRawRef.current !== pending.rawValue) {
|
|
148
|
+
reportedErrorRawRef.current = pending.rawValue;
|
|
149
|
+
onErrorRef.current?.(pending.error);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
128
152
|
const setValue = (0, import_react.useCallback)(
|
|
129
153
|
(value) => {
|
|
130
154
|
try {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
155
|
+
if (typeof window === "undefined") {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
let valueToStore;
|
|
159
|
+
if (value instanceof Function) {
|
|
160
|
+
const currentValue = (() => {
|
|
161
|
+
try {
|
|
162
|
+
const item = window.localStorage.getItem(key);
|
|
163
|
+
return item !== null ? deserializerRef.current(item) : resolveInitialValue(initialValueRef.current);
|
|
164
|
+
} catch {
|
|
165
|
+
return resolveInitialValue(initialValueRef.current);
|
|
136
166
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
})();
|
|
142
|
-
const valueToStore = value instanceof Function ? value(currentValue) : value;
|
|
143
|
-
if (typeof window !== "undefined") {
|
|
144
|
-
const serialized = serializerRef.current(valueToStore);
|
|
145
|
-
window.localStorage.setItem(key, serialized);
|
|
146
|
-
cacheRef.current = {
|
|
147
|
-
rawValue: serialized,
|
|
148
|
-
parsedValue: valueToStore
|
|
149
|
-
};
|
|
150
|
-
notifyListeners(key);
|
|
167
|
+
})();
|
|
168
|
+
valueToStore = value(currentValue);
|
|
169
|
+
} else {
|
|
170
|
+
valueToStore = value;
|
|
151
171
|
}
|
|
172
|
+
const serialized = serializerRef.current(valueToStore);
|
|
173
|
+
if (serialized === window.localStorage.getItem(key)) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
window.localStorage.setItem(key, serialized);
|
|
177
|
+
cacheRef.current = { rawValue: serialized, parsedValue: valueToStore };
|
|
178
|
+
notifyListeners(key);
|
|
152
179
|
} catch (error) {
|
|
153
180
|
onErrorRef.current?.(error);
|
|
154
181
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/useLocalStorage.ts","../src/store.ts"],"sourcesContent":["export {\n useLocalStorage,\n type UseLocalStorageOptions,\n type UseLocalStorageReturn,\n type InitialValue,\n} from \"./useLocalStorage\";\n","import { useCallback, useRef, useSyncExternalStore } from \"react\";\nimport { subscribe, notifyListeners } from \"./store\";\n\n/**\n * Type for initial value that can be a value or a function returning a value (lazy initialization)\n */\nexport type InitialValue<T> = T | (() => T);\n\n/**\n * Options for useLocalStorage hook\n */\nexport interface UseLocalStorageOptions<T> {\n /**\n * Custom serializer function for converting value to string\n * @default JSON.stringify\n */\n serializer?: (value: T) => string;\n /**\n * Custom deserializer function for parsing stored string to value\n * @default JSON.parse\n */\n deserializer?: (value: string) => T;\n /**\n * Whether to sync value across browser tabs via storage event\n * @default true\n */\n syncTabs?: boolean;\n /**\n * Callback function called when an error occurs\n */\n onError?: (error: Error) => void;\n}\n\n/**\n * Return type for useLocalStorage hook - tuple similar to useState\n */\nexport type UseLocalStorageReturn<T> = readonly [\n /** Current stored value */\n T,\n /** Function to update the value (same signature as useState setter) */\n React.Dispatch<React.SetStateAction<T>>,\n /** Function to remove the value from localStorage */\n () => void\n];\n\n/**\n * Helper function to resolve initial value (supports lazy initialization)\n */\nfunction resolveInitialValue<T>(initialValue: InitialValue<T>): T {\n return typeof initialValue === \"function\"\n ? (initialValue as () => T)()\n : initialValue;\n}\n\n/**\n * A hook for persisting state in localStorage with automatic synchronization.\n * Works like useState but persists the value in localStorage.\n *\n * Features:\n * - Same-tab synchronization: Multiple components using the same key will stay in sync\n * - Cross-tab synchronization: Changes in other tabs are automatically reflected\n * - SSR compatible: Works with Next.js, Remix, and other SSR frameworks\n *\n * @template T - The type of the stored value\n * @param key - The localStorage key to store the value under\n * @param initialValue - Initial value or function returning initial value (lazy initialization)\n * @param options - Configuration options for serialization, sync, and error handling\n * @returns A tuple of [storedValue, setValue, removeValue]\n *\n * @example\n * ```tsx\n * // Basic usage with string\n * function ThemeToggle() {\n * const [theme, setTheme, removeTheme] = useLocalStorage('theme', 'light');\n *\n * return (\n * <div>\n * <p>Current theme: {theme}</p>\n * <button onClick={() => setTheme('dark')}>Dark</button>\n * <button onClick={() => setTheme('light')}>Light</button>\n * <button onClick={removeTheme}>Reset</button>\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Same-tab synchronization - both components stay in sync\n * function ComponentA() {\n * const [user, setUser] = useLocalStorage('user', null);\n * return <button onClick={() => setUser({ name: 'John' })}>Set User</button>;\n * }\n *\n * function ComponentB() {\n * const [user] = useLocalStorage('user', null);\n * // Automatically updates when ComponentA calls setUser!\n * return <p>User: {user?.name}</p>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With object type\n * interface UserSettings {\n * notifications: boolean;\n * language: string;\n * }\n *\n * const [settings, setSettings] = useLocalStorage<UserSettings>('settings', {\n * notifications: true,\n * language: 'en',\n * });\n *\n * // Functional update\n * setSettings(prev => ({ ...prev, notifications: false }));\n * ```\n *\n * @example\n * ```tsx\n * // With lazy initialization\n * const [config, setConfig] = useLocalStorage('config', () => computeExpensiveDefault());\n * ```\n *\n * @example\n * ```tsx\n * // With custom serializer/deserializer\n * const [date, setDate] = useLocalStorage<Date>('lastVisit', new Date(), {\n * serializer: (d) => d.toISOString(),\n * deserializer: (s) => new Date(s),\n * });\n * ```\n *\n * @example\n * ```tsx\n * // With error handling\n * const [value, setValue] = useLocalStorage('key', 'default', {\n * onError: (error) => console.error('Storage error:', error),\n * });\n * ```\n */\nexport function useLocalStorage<T>(\n key: string,\n initialValue: InitialValue<T>,\n options: UseLocalStorageOptions<T> = {}\n): UseLocalStorageReturn<T> {\n const {\n serializer = JSON.stringify,\n deserializer = JSON.parse,\n syncTabs = true,\n onError,\n } = options;\n\n // Store options in refs for stable references and access to latest values\n const serializerRef = useRef(serializer);\n const deserializerRef = useRef(deserializer);\n const onErrorRef = useRef(onError);\n const initialValueRef = useRef(initialValue);\n\n serializerRef.current = serializer;\n deserializerRef.current = deserializer;\n onErrorRef.current = onError;\n initialValueRef.current = initialValue;\n\n // Cache for getSnapshot to ensure stable returns and prevent infinite loops\n // useSyncExternalStore requires getSnapshot to return the same reference\n // if the data hasn't changed\n const cacheRef = useRef<{ rawValue: string | null; parsedValue: T } | null>(\n null\n );\n\n // SSR check\n const isClient = typeof window !== \"undefined\";\n\n // Subscribe function for useSyncExternalStore\n // Handles both same-tab and cross-tab synchronization\n const subscribeToStore = useCallback(\n (onStoreChange: () => void) => {\n // Subscribe to same-tab changes via internal store\n const unsubscribeStore = subscribe(key, onStoreChange);\n\n // Subscribe to cross-tab changes via storage event\n let handleStorageEvent: ((event: StorageEvent) => void) | null = null;\n\n if (isClient && syncTabs) {\n handleStorageEvent = (event: StorageEvent) => {\n if (event.key === key) {\n onStoreChange();\n }\n };\n window.addEventListener(\"storage\", handleStorageEvent);\n }\n\n // Cleanup function\n return () => {\n unsubscribeStore();\n if (handleStorageEvent) {\n window.removeEventListener(\"storage\", handleStorageEvent);\n }\n };\n },\n [key, syncTabs, isClient]\n );\n\n // getSnapshot: Read current value from localStorage with caching\n const getSnapshot = useCallback((): T => {\n if (!isClient) {\n return resolveInitialValue(initialValueRef.current);\n }\n\n try {\n const rawValue = window.localStorage.getItem(key);\n\n // Check cache: if rawValue is the same, return cached parsed value\n if (cacheRef.current && cacheRef.current.rawValue === rawValue) {\n return cacheRef.current.parsedValue;\n }\n\n // Parse new value\n let parsedValue: T;\n if (rawValue !== null) {\n parsedValue = deserializerRef.current(rawValue);\n } else {\n parsedValue = resolveInitialValue(initialValueRef.current);\n }\n\n // Update cache\n cacheRef.current = { rawValue, parsedValue };\n\n return parsedValue;\n } catch (error) {\n onErrorRef.current?.(error as Error);\n const fallbackValue = resolveInitialValue(initialValueRef.current);\n cacheRef.current = { rawValue: null, parsedValue: fallbackValue };\n return fallbackValue;\n }\n }, [key, isClient]);\n\n // getServerSnapshot: Return initial value for SSR\n const getServerSnapshot = useCallback((): T => {\n return resolveInitialValue(initialValueRef.current);\n }, []);\n\n // Use useSyncExternalStore for synchronized state\n const storedValue = useSyncExternalStore(\n subscribeToStore,\n getSnapshot,\n getServerSnapshot\n );\n\n // setValue - stable reference that updates localStorage and notifies listeners\n const setValue = useCallback<React.Dispatch<React.SetStateAction<T>>>(\n (value) => {\n try {\n // Get current value for functional updates\n const currentValue = (() => {\n try {\n const item = window.localStorage.getItem(key);\n if (item !== null) {\n return deserializerRef.current(item);\n }\n return resolveInitialValue(initialValueRef.current);\n } catch {\n return resolveInitialValue(initialValueRef.current);\n }\n })();\n\n const valueToStore =\n value instanceof Function ? value(currentValue) : value;\n\n if (typeof window !== \"undefined\") {\n const serialized = serializerRef.current(valueToStore);\n window.localStorage.setItem(key, serialized);\n\n // Invalidate cache so next getSnapshot reads fresh value\n cacheRef.current = {\n rawValue: serialized,\n parsedValue: valueToStore,\n };\n\n // Notify all same-tab listeners\n notifyListeners(key);\n }\n } catch (error) {\n onErrorRef.current?.(error as Error);\n }\n },\n [key]\n );\n\n // removeValue - stable reference\n const removeValue = useCallback(() => {\n try {\n if (typeof window !== \"undefined\") {\n window.localStorage.removeItem(key);\n\n // Invalidate cache\n const initialVal = resolveInitialValue(initialValueRef.current);\n cacheRef.current = { rawValue: null, parsedValue: initialVal };\n\n // Notify all same-tab listeners\n notifyListeners(key);\n }\n } catch (error) {\n onErrorRef.current?.(error as Error);\n }\n }, [key]);\n\n return [storedValue, setValue, removeValue] as const;\n}\n","/**\n * Internal Store Manager for localStorage synchronization\n * This module manages listeners for same-tab synchronization across components\n * using the same localStorage key.\n *\n * @internal This module is not exported publicly\n */\n\n/** Map of key -> Set of listener callbacks */\nconst listeners = new Map<string, Set<() => void>>();\n\n/**\n * Subscribe a listener to changes for a specific key\n * @param key - The localStorage key to subscribe to\n * @param listener - Callback to invoke when the key's value changes\n * @returns Unsubscribe function\n */\nexport function subscribe(key: string, listener: () => void): () => void {\n if (!listeners.has(key)) {\n listeners.set(key, new Set());\n }\n\n const keyListeners = listeners.get(key)!;\n keyListeners.add(listener);\n\n return () => {\n keyListeners.delete(listener);\n\n // Cleanup: remove the key entry if no more listeners\n if (keyListeners.size === 0) {\n listeners.delete(key);\n }\n };\n}\n\n/**\n * Notify all listeners subscribed to a specific key\n * This is called when setValue or removeValue is invoked\n * to synchronize all components using the same key in the same tab\n *\n * @param key - The localStorage key that was updated\n */\nexport function notifyListeners(key: string): void {\n const keyListeners = listeners.get(key);\n if (keyListeners) {\n keyListeners.forEach((listener) => listener());\n }\n}\n\n/**\n * Get the count of listeners for a key (for testing purposes)\n * @internal\n */\nexport function getListenerCount(key: string): number {\n return listeners.get(key)?.size ?? 0;\n}\n\n/**\n * Clear all listeners (for testing purposes)\n * @internal\n */\nexport function clearAllListeners(): void {\n listeners.clear();\n}\n\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/useLocalStorage.ts","../src/store.ts"],"sourcesContent":["export {\n useLocalStorage,\n type UseLocalStorageOptions,\n type UseLocalStorageReturn,\n type InitialValue,\n} from \"./useLocalStorage\";\n","import { useCallback, useEffect, useRef, useSyncExternalStore } from \"react\";\nimport { subscribe, notifyListeners } from \"./store\";\n\n/**\n * Type for initial value that can be a value or a function returning a value (lazy initialization)\n */\nexport type InitialValue<T> = T | (() => T);\n\n/**\n * Options for useLocalStorage hook\n */\nexport interface UseLocalStorageOptions<T> {\n /**\n * Custom serializer function for converting value to string\n * @default JSON.stringify\n */\n serializer?: (value: T) => string;\n /**\n * Custom deserializer function for parsing stored string to value\n * @default JSON.parse\n */\n deserializer?: (value: string) => T;\n /**\n * Whether to sync value across browser tabs via storage event\n * @default true\n */\n syncTabs?: boolean;\n /**\n * Callback function called when an error occurs\n */\n onError?: (error: Error) => void;\n}\n\n/**\n * Return type for useLocalStorage hook - tuple similar to useState\n */\nexport type UseLocalStorageReturn<T> = readonly [\n /** Current stored value */\n T,\n /** Function to update the value (same signature as useState setter) */\n React.Dispatch<React.SetStateAction<T>>,\n /** Function to remove the value from localStorage */\n () => void\n];\n\n/**\n * Helper function to resolve initial value (supports lazy initialization)\n */\nfunction resolveInitialValue<T>(initialValue: InitialValue<T>): T {\n return typeof initialValue === \"function\"\n ? (initialValue as () => T)()\n : initialValue;\n}\n\n/**\n * A hook for persisting state in localStorage with automatic synchronization.\n * Works like useState but persists the value in localStorage.\n *\n * Features:\n * - Same-tab synchronization: Multiple components using the same key will stay in sync\n * - Cross-tab synchronization: Changes in other tabs are automatically reflected\n * - SSR compatible: Works with Next.js, Remix, and other SSR frameworks\n *\n * @template T - The type of the stored value\n * @param key - The localStorage key to store the value under\n * @param initialValue - Initial value or function returning initial value (lazy initialization)\n * @param options - Configuration options for serialization, sync, and error handling\n * @returns A tuple of [storedValue, setValue, removeValue]\n *\n * @example\n * ```tsx\n * // Basic usage with string\n * function ThemeToggle() {\n * const [theme, setTheme, removeTheme] = useLocalStorage('theme', 'light');\n *\n * return (\n * <div>\n * <p>Current theme: {theme}</p>\n * <button onClick={() => setTheme('dark')}>Dark</button>\n * <button onClick={() => setTheme('light')}>Light</button>\n * <button onClick={removeTheme}>Reset</button>\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Same-tab synchronization - both components stay in sync\n * function ComponentA() {\n * const [user, setUser] = useLocalStorage('user', null);\n * return <button onClick={() => setUser({ name: 'John' })}>Set User</button>;\n * }\n *\n * function ComponentB() {\n * const [user] = useLocalStorage('user', null);\n * // Automatically updates when ComponentA calls setUser!\n * return <p>User: {user?.name}</p>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With object type\n * interface UserSettings {\n * notifications: boolean;\n * language: string;\n * }\n *\n * const [settings, setSettings] = useLocalStorage<UserSettings>('settings', {\n * notifications: true,\n * language: 'en',\n * });\n *\n * // Functional update\n * setSettings(prev => ({ ...prev, notifications: false }));\n * ```\n *\n * @example\n * ```tsx\n * // With lazy initialization\n * const [config, setConfig] = useLocalStorage('config', () => computeExpensiveDefault());\n * ```\n *\n * @example\n * ```tsx\n * // With custom serializer/deserializer\n * const [date, setDate] = useLocalStorage<Date>('lastVisit', new Date(), {\n * serializer: (d) => d.toISOString(),\n * deserializer: (s) => new Date(s),\n * });\n * ```\n *\n * @example\n * ```tsx\n * // With error handling\n * const [value, setValue] = useLocalStorage('key', 'default', {\n * onError: (error) => console.error('Storage error:', error),\n * });\n * ```\n */\nexport function useLocalStorage<T>(\n key: string,\n initialValue: InitialValue<T>,\n options: UseLocalStorageOptions<T> = {}\n): UseLocalStorageReturn<T> {\n const {\n serializer = JSON.stringify,\n deserializer = JSON.parse,\n syncTabs = true,\n onError,\n } = options;\n\n // Store options in refs for stable references and access to latest values\n const serializerRef = useRef(serializer);\n const deserializerRef = useRef(deserializer);\n const onErrorRef = useRef(onError);\n const initialValueRef = useRef(initialValue);\n\n serializerRef.current = serializer;\n deserializerRef.current = deserializer;\n onErrorRef.current = onError;\n initialValueRef.current = initialValue;\n\n // Cache for getSnapshot to ensure stable returns and prevent infinite loops\n // useSyncExternalStore requires getSnapshot to return the same reference\n // if the data hasn't changed\n const cacheRef = useRef<{ rawValue: string | null; parsedValue: T } | null>(\n null\n );\n\n // Read errors are recorded here from the (pure) getSnapshot and flushed to\n // onError from a post-commit effect, so the user callback never fires during\n // render (which would double-fire under StrictMode).\n const readErrorRef = useRef<{ rawValue: string | null; error: Error } | null>(\n null\n );\n\n // Memoized server/initial snapshot so getServerSnapshot returns a stable\n // reference across calls (a fresh object each call triggers React's\n // \"getServerSnapshot should be cached\" infinite-loop warning).\n const serverSnapshotRef = useRef<{ value: T } | null>(null);\n const readServerSnapshot = useCallback((): T => {\n if (!serverSnapshotRef.current) {\n serverSnapshotRef.current = {\n value: resolveInitialValue(initialValueRef.current),\n };\n }\n return serverSnapshotRef.current.value;\n }, []);\n\n // SSR check\n const isClient = typeof window !== \"undefined\";\n\n // Subscribe function for useSyncExternalStore\n // Handles both same-tab and cross-tab synchronization\n const subscribeToStore = useCallback(\n (onStoreChange: () => void) => {\n // Subscribe to same-tab changes via internal store\n const unsubscribeStore = subscribe(key, onStoreChange);\n\n // Subscribe to cross-tab changes via storage event\n let handleStorageEvent: ((event: StorageEvent) => void) | null = null;\n\n if (isClient && syncTabs) {\n handleStorageEvent = (event: StorageEvent) => {\n // event.key === null is fired by localStorage.clear() and must also\n // reset this hook to its initial value.\n if (event.key === key || event.key === null) {\n onStoreChange();\n }\n };\n window.addEventListener(\"storage\", handleStorageEvent);\n }\n\n // Cleanup function\n return () => {\n unsubscribeStore();\n if (handleStorageEvent) {\n window.removeEventListener(\"storage\", handleStorageEvent);\n }\n };\n },\n [key, syncTabs, isClient]\n );\n\n // getSnapshot: Read current value from localStorage with caching.\n // MUST be pure and return a stable reference when the underlying raw string\n // is unchanged — otherwise useSyncExternalStore loops forever.\n const getSnapshot = useCallback((): T => {\n if (!isClient) {\n return readServerSnapshot();\n }\n\n // Read the raw string first; key the cache on the ACTUAL value so a corrupt\n // entry still produces a stable cache hit on the next read.\n let rawValue: string | null;\n try {\n rawValue = window.localStorage.getItem(key);\n } catch (error) {\n if (cacheRef.current && cacheRef.current.rawValue === null) {\n return cacheRef.current.parsedValue;\n }\n const fallbackValue = resolveInitialValue(initialValueRef.current);\n cacheRef.current = { rawValue: null, parsedValue: fallbackValue };\n readErrorRef.current = { rawValue: null, error: error as Error };\n return fallbackValue;\n }\n\n // Cache hit: same raw string as last read → return the same reference.\n if (cacheRef.current && cacheRef.current.rawValue === rawValue) {\n return cacheRef.current.parsedValue;\n }\n\n try {\n const parsedValue =\n rawValue !== null\n ? deserializerRef.current(rawValue)\n : resolveInitialValue(initialValueRef.current);\n cacheRef.current = { rawValue, parsedValue };\n return parsedValue;\n } catch (error) {\n // Corrupt value: cache the fallback keyed on the REAL rawValue so the next\n // getSnapshot is a cache hit (caching null here would loop forever). Record\n // the error for the post-commit flush instead of calling onError in render.\n const fallbackValue = resolveInitialValue(initialValueRef.current);\n cacheRef.current = { rawValue, parsedValue: fallbackValue };\n readErrorRef.current = { rawValue, error: error as Error };\n return fallbackValue;\n }\n }, [key, isClient, readServerSnapshot]);\n\n // getServerSnapshot: stable initial value for SSR.\n const getServerSnapshot = readServerSnapshot;\n\n // Use useSyncExternalStore for synchronized state\n const storedValue = useSyncExternalStore(\n subscribeToStore,\n getSnapshot,\n getServerSnapshot\n );\n\n // Flush read errors after commit (getSnapshot stays pure). Deduped by raw\n // value so onError fires once per distinct corrupt entry, not once per render.\n const reportedErrorRawRef = useRef<string | null | undefined>(undefined);\n useEffect(() => {\n const pending = readErrorRef.current;\n if (pending && reportedErrorRawRef.current !== pending.rawValue) {\n reportedErrorRawRef.current = pending.rawValue;\n onErrorRef.current?.(pending.error);\n }\n });\n\n // setValue - stable reference that updates localStorage and notifies listeners\n const setValue = useCallback<React.Dispatch<React.SetStateAction<T>>>(\n (value) => {\n try {\n if (typeof window === \"undefined\") {\n return;\n }\n\n let valueToStore: T;\n if (value instanceof Function) {\n // Only read/deserialize the current value for functional updates.\n const currentValue = (() => {\n try {\n const item = window.localStorage.getItem(key);\n return item !== null\n ? deserializerRef.current(item)\n : resolveInitialValue(initialValueRef.current);\n } catch {\n return resolveInitialValue(initialValueRef.current);\n }\n })();\n valueToStore = (value as (prev: T) => T)(currentValue);\n } else {\n valueToStore = value;\n }\n\n const serialized = serializerRef.current(valueToStore);\n\n // No-op skip: unchanged serialized value → no write, no re-render churn.\n if (serialized === window.localStorage.getItem(key)) {\n return;\n }\n\n window.localStorage.setItem(key, serialized);\n\n // Prime the cache so the next getSnapshot returns this exact reference.\n cacheRef.current = { rawValue: serialized, parsedValue: valueToStore };\n\n // Notify all same-tab listeners\n notifyListeners(key);\n } catch (error) {\n onErrorRef.current?.(error as Error);\n }\n },\n [key]\n );\n\n // removeValue - stable reference\n const removeValue = useCallback(() => {\n try {\n if (typeof window !== \"undefined\") {\n window.localStorage.removeItem(key);\n\n // Invalidate cache\n const initialVal = resolveInitialValue(initialValueRef.current);\n cacheRef.current = { rawValue: null, parsedValue: initialVal };\n\n // Notify all same-tab listeners\n notifyListeners(key);\n }\n } catch (error) {\n onErrorRef.current?.(error as Error);\n }\n }, [key]);\n\n return [storedValue, setValue, removeValue] as const;\n}\n","/**\n * Internal Store Manager for localStorage synchronization\n * This module manages listeners for same-tab synchronization across components\n * using the same localStorage key.\n *\n * @internal This module is not exported publicly\n */\n\n/** Map of key -> Set of listener callbacks */\nconst listeners = new Map<string, Set<() => void>>();\n\n/**\n * Subscribe a listener to changes for a specific key\n * @param key - The localStorage key to subscribe to\n * @param listener - Callback to invoke when the key's value changes\n * @returns Unsubscribe function\n */\nexport function subscribe(key: string, listener: () => void): () => void {\n if (!listeners.has(key)) {\n listeners.set(key, new Set());\n }\n\n const keyListeners = listeners.get(key)!;\n keyListeners.add(listener);\n\n return () => {\n keyListeners.delete(listener);\n\n // Cleanup: remove the key entry if no more listeners\n if (keyListeners.size === 0) {\n listeners.delete(key);\n }\n };\n}\n\n/**\n * Notify all listeners subscribed to a specific key\n * This is called when setValue or removeValue is invoked\n * to synchronize all components using the same key in the same tab\n *\n * @param key - The localStorage key that was updated\n */\nexport function notifyListeners(key: string): void {\n const keyListeners = listeners.get(key);\n if (keyListeners) {\n keyListeners.forEach((listener) => listener());\n }\n}\n\n/**\n * Get the count of listeners for a key (for testing purposes)\n * @internal\n */\nexport function getListenerCount(key: string): number {\n return listeners.get(key)?.size ?? 0;\n}\n\n/**\n * Clear all listeners (for testing purposes)\n * @internal\n */\nexport function clearAllListeners(): void {\n listeners.clear();\n}\n\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/useLocalStorage.ts
|
|
2
|
-
import { useCallback, useRef, useSyncExternalStore } from "react";
|
|
2
|
+
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
3
3
|
|
|
4
4
|
// src/store.ts
|
|
5
5
|
var listeners = /* @__PURE__ */ new Map();
|
|
@@ -45,6 +45,18 @@ function useLocalStorage(key, initialValue, options = {}) {
|
|
|
45
45
|
const cacheRef = useRef(
|
|
46
46
|
null
|
|
47
47
|
);
|
|
48
|
+
const readErrorRef = useRef(
|
|
49
|
+
null
|
|
50
|
+
);
|
|
51
|
+
const serverSnapshotRef = useRef(null);
|
|
52
|
+
const readServerSnapshot = useCallback(() => {
|
|
53
|
+
if (!serverSnapshotRef.current) {
|
|
54
|
+
serverSnapshotRef.current = {
|
|
55
|
+
value: resolveInitialValue(initialValueRef.current)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return serverSnapshotRef.current.value;
|
|
59
|
+
}, []);
|
|
48
60
|
const isClient = typeof window !== "undefined";
|
|
49
61
|
const subscribeToStore = useCallback(
|
|
50
62
|
(onStoreChange) => {
|
|
@@ -52,7 +64,7 @@ function useLocalStorage(key, initialValue, options = {}) {
|
|
|
52
64
|
let handleStorageEvent = null;
|
|
53
65
|
if (isClient && syncTabs) {
|
|
54
66
|
handleStorageEvent = (event) => {
|
|
55
|
-
if (event.key === key) {
|
|
67
|
+
if (event.key === key || event.key === null) {
|
|
56
68
|
onStoreChange();
|
|
57
69
|
}
|
|
58
70
|
};
|
|
@@ -69,60 +81,75 @@ function useLocalStorage(key, initialValue, options = {}) {
|
|
|
69
81
|
);
|
|
70
82
|
const getSnapshot = useCallback(() => {
|
|
71
83
|
if (!isClient) {
|
|
72
|
-
return
|
|
84
|
+
return readServerSnapshot();
|
|
73
85
|
}
|
|
86
|
+
let rawValue;
|
|
74
87
|
try {
|
|
75
|
-
|
|
76
|
-
|
|
88
|
+
rawValue = window.localStorage.getItem(key);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (cacheRef.current && cacheRef.current.rawValue === null) {
|
|
77
91
|
return cacheRef.current.parsedValue;
|
|
78
92
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
const fallbackValue = resolveInitialValue(initialValueRef.current);
|
|
94
|
+
cacheRef.current = { rawValue: null, parsedValue: fallbackValue };
|
|
95
|
+
readErrorRef.current = { rawValue: null, error };
|
|
96
|
+
return fallbackValue;
|
|
97
|
+
}
|
|
98
|
+
if (cacheRef.current && cacheRef.current.rawValue === rawValue) {
|
|
99
|
+
return cacheRef.current.parsedValue;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const parsedValue = rawValue !== null ? deserializerRef.current(rawValue) : resolveInitialValue(initialValueRef.current);
|
|
85
103
|
cacheRef.current = { rawValue, parsedValue };
|
|
86
104
|
return parsedValue;
|
|
87
105
|
} catch (error) {
|
|
88
|
-
onErrorRef.current?.(error);
|
|
89
106
|
const fallbackValue = resolveInitialValue(initialValueRef.current);
|
|
90
|
-
cacheRef.current = { rawValue
|
|
107
|
+
cacheRef.current = { rawValue, parsedValue: fallbackValue };
|
|
108
|
+
readErrorRef.current = { rawValue, error };
|
|
91
109
|
return fallbackValue;
|
|
92
110
|
}
|
|
93
|
-
}, [key, isClient]);
|
|
94
|
-
const getServerSnapshot =
|
|
95
|
-
return resolveInitialValue(initialValueRef.current);
|
|
96
|
-
}, []);
|
|
111
|
+
}, [key, isClient, readServerSnapshot]);
|
|
112
|
+
const getServerSnapshot = readServerSnapshot;
|
|
97
113
|
const storedValue = useSyncExternalStore(
|
|
98
114
|
subscribeToStore,
|
|
99
115
|
getSnapshot,
|
|
100
116
|
getServerSnapshot
|
|
101
117
|
);
|
|
118
|
+
const reportedErrorRawRef = useRef(void 0);
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
const pending = readErrorRef.current;
|
|
121
|
+
if (pending && reportedErrorRawRef.current !== pending.rawValue) {
|
|
122
|
+
reportedErrorRawRef.current = pending.rawValue;
|
|
123
|
+
onErrorRef.current?.(pending.error);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
102
126
|
const setValue = useCallback(
|
|
103
127
|
(value) => {
|
|
104
128
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
129
|
+
if (typeof window === "undefined") {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
let valueToStore;
|
|
133
|
+
if (value instanceof Function) {
|
|
134
|
+
const currentValue = (() => {
|
|
135
|
+
try {
|
|
136
|
+
const item = window.localStorage.getItem(key);
|
|
137
|
+
return item !== null ? deserializerRef.current(item) : resolveInitialValue(initialValueRef.current);
|
|
138
|
+
} catch {
|
|
139
|
+
return resolveInitialValue(initialValueRef.current);
|
|
110
140
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
})();
|
|
116
|
-
const valueToStore = value instanceof Function ? value(currentValue) : value;
|
|
117
|
-
if (typeof window !== "undefined") {
|
|
118
|
-
const serialized = serializerRef.current(valueToStore);
|
|
119
|
-
window.localStorage.setItem(key, serialized);
|
|
120
|
-
cacheRef.current = {
|
|
121
|
-
rawValue: serialized,
|
|
122
|
-
parsedValue: valueToStore
|
|
123
|
-
};
|
|
124
|
-
notifyListeners(key);
|
|
141
|
+
})();
|
|
142
|
+
valueToStore = value(currentValue);
|
|
143
|
+
} else {
|
|
144
|
+
valueToStore = value;
|
|
125
145
|
}
|
|
146
|
+
const serialized = serializerRef.current(valueToStore);
|
|
147
|
+
if (serialized === window.localStorage.getItem(key)) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
window.localStorage.setItem(key, serialized);
|
|
151
|
+
cacheRef.current = { rawValue: serialized, parsedValue: valueToStore };
|
|
152
|
+
notifyListeners(key);
|
|
126
153
|
} catch (error) {
|
|
127
154
|
onErrorRef.current?.(error);
|
|
128
155
|
}
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/useLocalStorage.ts","../src/store.ts"],"sourcesContent":["import { useCallback, useRef, useSyncExternalStore } from \"react\";\nimport { subscribe, notifyListeners } from \"./store\";\n\n/**\n * Type for initial value that can be a value or a function returning a value (lazy initialization)\n */\nexport type InitialValue<T> = T | (() => T);\n\n/**\n * Options for useLocalStorage hook\n */\nexport interface UseLocalStorageOptions<T> {\n /**\n * Custom serializer function for converting value to string\n * @default JSON.stringify\n */\n serializer?: (value: T) => string;\n /**\n * Custom deserializer function for parsing stored string to value\n * @default JSON.parse\n */\n deserializer?: (value: string) => T;\n /**\n * Whether to sync value across browser tabs via storage event\n * @default true\n */\n syncTabs?: boolean;\n /**\n * Callback function called when an error occurs\n */\n onError?: (error: Error) => void;\n}\n\n/**\n * Return type for useLocalStorage hook - tuple similar to useState\n */\nexport type UseLocalStorageReturn<T> = readonly [\n /** Current stored value */\n T,\n /** Function to update the value (same signature as useState setter) */\n React.Dispatch<React.SetStateAction<T>>,\n /** Function to remove the value from localStorage */\n () => void\n];\n\n/**\n * Helper function to resolve initial value (supports lazy initialization)\n */\nfunction resolveInitialValue<T>(initialValue: InitialValue<T>): T {\n return typeof initialValue === \"function\"\n ? (initialValue as () => T)()\n : initialValue;\n}\n\n/**\n * A hook for persisting state in localStorage with automatic synchronization.\n * Works like useState but persists the value in localStorage.\n *\n * Features:\n * - Same-tab synchronization: Multiple components using the same key will stay in sync\n * - Cross-tab synchronization: Changes in other tabs are automatically reflected\n * - SSR compatible: Works with Next.js, Remix, and other SSR frameworks\n *\n * @template T - The type of the stored value\n * @param key - The localStorage key to store the value under\n * @param initialValue - Initial value or function returning initial value (lazy initialization)\n * @param options - Configuration options for serialization, sync, and error handling\n * @returns A tuple of [storedValue, setValue, removeValue]\n *\n * @example\n * ```tsx\n * // Basic usage with string\n * function ThemeToggle() {\n * const [theme, setTheme, removeTheme] = useLocalStorage('theme', 'light');\n *\n * return (\n * <div>\n * <p>Current theme: {theme}</p>\n * <button onClick={() => setTheme('dark')}>Dark</button>\n * <button onClick={() => setTheme('light')}>Light</button>\n * <button onClick={removeTheme}>Reset</button>\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Same-tab synchronization - both components stay in sync\n * function ComponentA() {\n * const [user, setUser] = useLocalStorage('user', null);\n * return <button onClick={() => setUser({ name: 'John' })}>Set User</button>;\n * }\n *\n * function ComponentB() {\n * const [user] = useLocalStorage('user', null);\n * // Automatically updates when ComponentA calls setUser!\n * return <p>User: {user?.name}</p>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With object type\n * interface UserSettings {\n * notifications: boolean;\n * language: string;\n * }\n *\n * const [settings, setSettings] = useLocalStorage<UserSettings>('settings', {\n * notifications: true,\n * language: 'en',\n * });\n *\n * // Functional update\n * setSettings(prev => ({ ...prev, notifications: false }));\n * ```\n *\n * @example\n * ```tsx\n * // With lazy initialization\n * const [config, setConfig] = useLocalStorage('config', () => computeExpensiveDefault());\n * ```\n *\n * @example\n * ```tsx\n * // With custom serializer/deserializer\n * const [date, setDate] = useLocalStorage<Date>('lastVisit', new Date(), {\n * serializer: (d) => d.toISOString(),\n * deserializer: (s) => new Date(s),\n * });\n * ```\n *\n * @example\n * ```tsx\n * // With error handling\n * const [value, setValue] = useLocalStorage('key', 'default', {\n * onError: (error) => console.error('Storage error:', error),\n * });\n * ```\n */\nexport function useLocalStorage<T>(\n key: string,\n initialValue: InitialValue<T>,\n options: UseLocalStorageOptions<T> = {}\n): UseLocalStorageReturn<T> {\n const {\n serializer = JSON.stringify,\n deserializer = JSON.parse,\n syncTabs = true,\n onError,\n } = options;\n\n // Store options in refs for stable references and access to latest values\n const serializerRef = useRef(serializer);\n const deserializerRef = useRef(deserializer);\n const onErrorRef = useRef(onError);\n const initialValueRef = useRef(initialValue);\n\n serializerRef.current = serializer;\n deserializerRef.current = deserializer;\n onErrorRef.current = onError;\n initialValueRef.current = initialValue;\n\n // Cache for getSnapshot to ensure stable returns and prevent infinite loops\n // useSyncExternalStore requires getSnapshot to return the same reference\n // if the data hasn't changed\n const cacheRef = useRef<{ rawValue: string | null; parsedValue: T } | null>(\n null\n );\n\n // SSR check\n const isClient = typeof window !== \"undefined\";\n\n // Subscribe function for useSyncExternalStore\n // Handles both same-tab and cross-tab synchronization\n const subscribeToStore = useCallback(\n (onStoreChange: () => void) => {\n // Subscribe to same-tab changes via internal store\n const unsubscribeStore = subscribe(key, onStoreChange);\n\n // Subscribe to cross-tab changes via storage event\n let handleStorageEvent: ((event: StorageEvent) => void) | null = null;\n\n if (isClient && syncTabs) {\n handleStorageEvent = (event: StorageEvent) => {\n if (event.key === key) {\n onStoreChange();\n }\n };\n window.addEventListener(\"storage\", handleStorageEvent);\n }\n\n // Cleanup function\n return () => {\n unsubscribeStore();\n if (handleStorageEvent) {\n window.removeEventListener(\"storage\", handleStorageEvent);\n }\n };\n },\n [key, syncTabs, isClient]\n );\n\n // getSnapshot: Read current value from localStorage with caching\n const getSnapshot = useCallback((): T => {\n if (!isClient) {\n return resolveInitialValue(initialValueRef.current);\n }\n\n try {\n const rawValue = window.localStorage.getItem(key);\n\n // Check cache: if rawValue is the same, return cached parsed value\n if (cacheRef.current && cacheRef.current.rawValue === rawValue) {\n return cacheRef.current.parsedValue;\n }\n\n // Parse new value\n let parsedValue: T;\n if (rawValue !== null) {\n parsedValue = deserializerRef.current(rawValue);\n } else {\n parsedValue = resolveInitialValue(initialValueRef.current);\n }\n\n // Update cache\n cacheRef.current = { rawValue, parsedValue };\n\n return parsedValue;\n } catch (error) {\n onErrorRef.current?.(error as Error);\n const fallbackValue = resolveInitialValue(initialValueRef.current);\n cacheRef.current = { rawValue: null, parsedValue: fallbackValue };\n return fallbackValue;\n }\n }, [key, isClient]);\n\n // getServerSnapshot: Return initial value for SSR\n const getServerSnapshot = useCallback((): T => {\n return resolveInitialValue(initialValueRef.current);\n }, []);\n\n // Use useSyncExternalStore for synchronized state\n const storedValue = useSyncExternalStore(\n subscribeToStore,\n getSnapshot,\n getServerSnapshot\n );\n\n // setValue - stable reference that updates localStorage and notifies listeners\n const setValue = useCallback<React.Dispatch<React.SetStateAction<T>>>(\n (value) => {\n try {\n // Get current value for functional updates\n const currentValue = (() => {\n try {\n const item = window.localStorage.getItem(key);\n if (item !== null) {\n return deserializerRef.current(item);\n }\n return resolveInitialValue(initialValueRef.current);\n } catch {\n return resolveInitialValue(initialValueRef.current);\n }\n })();\n\n const valueToStore =\n value instanceof Function ? value(currentValue) : value;\n\n if (typeof window !== \"undefined\") {\n const serialized = serializerRef.current(valueToStore);\n window.localStorage.setItem(key, serialized);\n\n // Invalidate cache so next getSnapshot reads fresh value\n cacheRef.current = {\n rawValue: serialized,\n parsedValue: valueToStore,\n };\n\n // Notify all same-tab listeners\n notifyListeners(key);\n }\n } catch (error) {\n onErrorRef.current?.(error as Error);\n }\n },\n [key]\n );\n\n // removeValue - stable reference\n const removeValue = useCallback(() => {\n try {\n if (typeof window !== \"undefined\") {\n window.localStorage.removeItem(key);\n\n // Invalidate cache\n const initialVal = resolveInitialValue(initialValueRef.current);\n cacheRef.current = { rawValue: null, parsedValue: initialVal };\n\n // Notify all same-tab listeners\n notifyListeners(key);\n }\n } catch (error) {\n onErrorRef.current?.(error as Error);\n }\n }, [key]);\n\n return [storedValue, setValue, removeValue] as const;\n}\n","/**\n * Internal Store Manager for localStorage synchronization\n * This module manages listeners for same-tab synchronization across components\n * using the same localStorage key.\n *\n * @internal This module is not exported publicly\n */\n\n/** Map of key -> Set of listener callbacks */\nconst listeners = new Map<string, Set<() => void>>();\n\n/**\n * Subscribe a listener to changes for a specific key\n * @param key - The localStorage key to subscribe to\n * @param listener - Callback to invoke when the key's value changes\n * @returns Unsubscribe function\n */\nexport function subscribe(key: string, listener: () => void): () => void {\n if (!listeners.has(key)) {\n listeners.set(key, new Set());\n }\n\n const keyListeners = listeners.get(key)!;\n keyListeners.add(listener);\n\n return () => {\n keyListeners.delete(listener);\n\n // Cleanup: remove the key entry if no more listeners\n if (keyListeners.size === 0) {\n listeners.delete(key);\n }\n };\n}\n\n/**\n * Notify all listeners subscribed to a specific key\n * This is called when setValue or removeValue is invoked\n * to synchronize all components using the same key in the same tab\n *\n * @param key - The localStorage key that was updated\n */\nexport function notifyListeners(key: string): void {\n const keyListeners = listeners.get(key);\n if (keyListeners) {\n keyListeners.forEach((listener) => listener());\n }\n}\n\n/**\n * Get the count of listeners for a key (for testing purposes)\n * @internal\n */\nexport function getListenerCount(key: string): number {\n return listeners.get(key)?.size ?? 0;\n}\n\n/**\n * Clear all listeners (for testing purposes)\n * @internal\n */\nexport function clearAllListeners(): void {\n listeners.clear();\n}\n\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/useLocalStorage.ts","../src/store.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useSyncExternalStore } from \"react\";\nimport { subscribe, notifyListeners } from \"./store\";\n\n/**\n * Type for initial value that can be a value or a function returning a value (lazy initialization)\n */\nexport type InitialValue<T> = T | (() => T);\n\n/**\n * Options for useLocalStorage hook\n */\nexport interface UseLocalStorageOptions<T> {\n /**\n * Custom serializer function for converting value to string\n * @default JSON.stringify\n */\n serializer?: (value: T) => string;\n /**\n * Custom deserializer function for parsing stored string to value\n * @default JSON.parse\n */\n deserializer?: (value: string) => T;\n /**\n * Whether to sync value across browser tabs via storage event\n * @default true\n */\n syncTabs?: boolean;\n /**\n * Callback function called when an error occurs\n */\n onError?: (error: Error) => void;\n}\n\n/**\n * Return type for useLocalStorage hook - tuple similar to useState\n */\nexport type UseLocalStorageReturn<T> = readonly [\n /** Current stored value */\n T,\n /** Function to update the value (same signature as useState setter) */\n React.Dispatch<React.SetStateAction<T>>,\n /** Function to remove the value from localStorage */\n () => void\n];\n\n/**\n * Helper function to resolve initial value (supports lazy initialization)\n */\nfunction resolveInitialValue<T>(initialValue: InitialValue<T>): T {\n return typeof initialValue === \"function\"\n ? (initialValue as () => T)()\n : initialValue;\n}\n\n/**\n * A hook for persisting state in localStorage with automatic synchronization.\n * Works like useState but persists the value in localStorage.\n *\n * Features:\n * - Same-tab synchronization: Multiple components using the same key will stay in sync\n * - Cross-tab synchronization: Changes in other tabs are automatically reflected\n * - SSR compatible: Works with Next.js, Remix, and other SSR frameworks\n *\n * @template T - The type of the stored value\n * @param key - The localStorage key to store the value under\n * @param initialValue - Initial value or function returning initial value (lazy initialization)\n * @param options - Configuration options for serialization, sync, and error handling\n * @returns A tuple of [storedValue, setValue, removeValue]\n *\n * @example\n * ```tsx\n * // Basic usage with string\n * function ThemeToggle() {\n * const [theme, setTheme, removeTheme] = useLocalStorage('theme', 'light');\n *\n * return (\n * <div>\n * <p>Current theme: {theme}</p>\n * <button onClick={() => setTheme('dark')}>Dark</button>\n * <button onClick={() => setTheme('light')}>Light</button>\n * <button onClick={removeTheme}>Reset</button>\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Same-tab synchronization - both components stay in sync\n * function ComponentA() {\n * const [user, setUser] = useLocalStorage('user', null);\n * return <button onClick={() => setUser({ name: 'John' })}>Set User</button>;\n * }\n *\n * function ComponentB() {\n * const [user] = useLocalStorage('user', null);\n * // Automatically updates when ComponentA calls setUser!\n * return <p>User: {user?.name}</p>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With object type\n * interface UserSettings {\n * notifications: boolean;\n * language: string;\n * }\n *\n * const [settings, setSettings] = useLocalStorage<UserSettings>('settings', {\n * notifications: true,\n * language: 'en',\n * });\n *\n * // Functional update\n * setSettings(prev => ({ ...prev, notifications: false }));\n * ```\n *\n * @example\n * ```tsx\n * // With lazy initialization\n * const [config, setConfig] = useLocalStorage('config', () => computeExpensiveDefault());\n * ```\n *\n * @example\n * ```tsx\n * // With custom serializer/deserializer\n * const [date, setDate] = useLocalStorage<Date>('lastVisit', new Date(), {\n * serializer: (d) => d.toISOString(),\n * deserializer: (s) => new Date(s),\n * });\n * ```\n *\n * @example\n * ```tsx\n * // With error handling\n * const [value, setValue] = useLocalStorage('key', 'default', {\n * onError: (error) => console.error('Storage error:', error),\n * });\n * ```\n */\nexport function useLocalStorage<T>(\n key: string,\n initialValue: InitialValue<T>,\n options: UseLocalStorageOptions<T> = {}\n): UseLocalStorageReturn<T> {\n const {\n serializer = JSON.stringify,\n deserializer = JSON.parse,\n syncTabs = true,\n onError,\n } = options;\n\n // Store options in refs for stable references and access to latest values\n const serializerRef = useRef(serializer);\n const deserializerRef = useRef(deserializer);\n const onErrorRef = useRef(onError);\n const initialValueRef = useRef(initialValue);\n\n serializerRef.current = serializer;\n deserializerRef.current = deserializer;\n onErrorRef.current = onError;\n initialValueRef.current = initialValue;\n\n // Cache for getSnapshot to ensure stable returns and prevent infinite loops\n // useSyncExternalStore requires getSnapshot to return the same reference\n // if the data hasn't changed\n const cacheRef = useRef<{ rawValue: string | null; parsedValue: T } | null>(\n null\n );\n\n // Read errors are recorded here from the (pure) getSnapshot and flushed to\n // onError from a post-commit effect, so the user callback never fires during\n // render (which would double-fire under StrictMode).\n const readErrorRef = useRef<{ rawValue: string | null; error: Error } | null>(\n null\n );\n\n // Memoized server/initial snapshot so getServerSnapshot returns a stable\n // reference across calls (a fresh object each call triggers React's\n // \"getServerSnapshot should be cached\" infinite-loop warning).\n const serverSnapshotRef = useRef<{ value: T } | null>(null);\n const readServerSnapshot = useCallback((): T => {\n if (!serverSnapshotRef.current) {\n serverSnapshotRef.current = {\n value: resolveInitialValue(initialValueRef.current),\n };\n }\n return serverSnapshotRef.current.value;\n }, []);\n\n // SSR check\n const isClient = typeof window !== \"undefined\";\n\n // Subscribe function for useSyncExternalStore\n // Handles both same-tab and cross-tab synchronization\n const subscribeToStore = useCallback(\n (onStoreChange: () => void) => {\n // Subscribe to same-tab changes via internal store\n const unsubscribeStore = subscribe(key, onStoreChange);\n\n // Subscribe to cross-tab changes via storage event\n let handleStorageEvent: ((event: StorageEvent) => void) | null = null;\n\n if (isClient && syncTabs) {\n handleStorageEvent = (event: StorageEvent) => {\n // event.key === null is fired by localStorage.clear() and must also\n // reset this hook to its initial value.\n if (event.key === key || event.key === null) {\n onStoreChange();\n }\n };\n window.addEventListener(\"storage\", handleStorageEvent);\n }\n\n // Cleanup function\n return () => {\n unsubscribeStore();\n if (handleStorageEvent) {\n window.removeEventListener(\"storage\", handleStorageEvent);\n }\n };\n },\n [key, syncTabs, isClient]\n );\n\n // getSnapshot: Read current value from localStorage with caching.\n // MUST be pure and return a stable reference when the underlying raw string\n // is unchanged — otherwise useSyncExternalStore loops forever.\n const getSnapshot = useCallback((): T => {\n if (!isClient) {\n return readServerSnapshot();\n }\n\n // Read the raw string first; key the cache on the ACTUAL value so a corrupt\n // entry still produces a stable cache hit on the next read.\n let rawValue: string | null;\n try {\n rawValue = window.localStorage.getItem(key);\n } catch (error) {\n if (cacheRef.current && cacheRef.current.rawValue === null) {\n return cacheRef.current.parsedValue;\n }\n const fallbackValue = resolveInitialValue(initialValueRef.current);\n cacheRef.current = { rawValue: null, parsedValue: fallbackValue };\n readErrorRef.current = { rawValue: null, error: error as Error };\n return fallbackValue;\n }\n\n // Cache hit: same raw string as last read → return the same reference.\n if (cacheRef.current && cacheRef.current.rawValue === rawValue) {\n return cacheRef.current.parsedValue;\n }\n\n try {\n const parsedValue =\n rawValue !== null\n ? deserializerRef.current(rawValue)\n : resolveInitialValue(initialValueRef.current);\n cacheRef.current = { rawValue, parsedValue };\n return parsedValue;\n } catch (error) {\n // Corrupt value: cache the fallback keyed on the REAL rawValue so the next\n // getSnapshot is a cache hit (caching null here would loop forever). Record\n // the error for the post-commit flush instead of calling onError in render.\n const fallbackValue = resolveInitialValue(initialValueRef.current);\n cacheRef.current = { rawValue, parsedValue: fallbackValue };\n readErrorRef.current = { rawValue, error: error as Error };\n return fallbackValue;\n }\n }, [key, isClient, readServerSnapshot]);\n\n // getServerSnapshot: stable initial value for SSR.\n const getServerSnapshot = readServerSnapshot;\n\n // Use useSyncExternalStore for synchronized state\n const storedValue = useSyncExternalStore(\n subscribeToStore,\n getSnapshot,\n getServerSnapshot\n );\n\n // Flush read errors after commit (getSnapshot stays pure). Deduped by raw\n // value so onError fires once per distinct corrupt entry, not once per render.\n const reportedErrorRawRef = useRef<string | null | undefined>(undefined);\n useEffect(() => {\n const pending = readErrorRef.current;\n if (pending && reportedErrorRawRef.current !== pending.rawValue) {\n reportedErrorRawRef.current = pending.rawValue;\n onErrorRef.current?.(pending.error);\n }\n });\n\n // setValue - stable reference that updates localStorage and notifies listeners\n const setValue = useCallback<React.Dispatch<React.SetStateAction<T>>>(\n (value) => {\n try {\n if (typeof window === \"undefined\") {\n return;\n }\n\n let valueToStore: T;\n if (value instanceof Function) {\n // Only read/deserialize the current value for functional updates.\n const currentValue = (() => {\n try {\n const item = window.localStorage.getItem(key);\n return item !== null\n ? deserializerRef.current(item)\n : resolveInitialValue(initialValueRef.current);\n } catch {\n return resolveInitialValue(initialValueRef.current);\n }\n })();\n valueToStore = (value as (prev: T) => T)(currentValue);\n } else {\n valueToStore = value;\n }\n\n const serialized = serializerRef.current(valueToStore);\n\n // No-op skip: unchanged serialized value → no write, no re-render churn.\n if (serialized === window.localStorage.getItem(key)) {\n return;\n }\n\n window.localStorage.setItem(key, serialized);\n\n // Prime the cache so the next getSnapshot returns this exact reference.\n cacheRef.current = { rawValue: serialized, parsedValue: valueToStore };\n\n // Notify all same-tab listeners\n notifyListeners(key);\n } catch (error) {\n onErrorRef.current?.(error as Error);\n }\n },\n [key]\n );\n\n // removeValue - stable reference\n const removeValue = useCallback(() => {\n try {\n if (typeof window !== \"undefined\") {\n window.localStorage.removeItem(key);\n\n // Invalidate cache\n const initialVal = resolveInitialValue(initialValueRef.current);\n cacheRef.current = { rawValue: null, parsedValue: initialVal };\n\n // Notify all same-tab listeners\n notifyListeners(key);\n }\n } catch (error) {\n onErrorRef.current?.(error as Error);\n }\n }, [key]);\n\n return [storedValue, setValue, removeValue] as const;\n}\n","/**\n * Internal Store Manager for localStorage synchronization\n * This module manages listeners for same-tab synchronization across components\n * using the same localStorage key.\n *\n * @internal This module is not exported publicly\n */\n\n/** Map of key -> Set of listener callbacks */\nconst listeners = new Map<string, Set<() => void>>();\n\n/**\n * Subscribe a listener to changes for a specific key\n * @param key - The localStorage key to subscribe to\n * @param listener - Callback to invoke when the key's value changes\n * @returns Unsubscribe function\n */\nexport function subscribe(key: string, listener: () => void): () => void {\n if (!listeners.has(key)) {\n listeners.set(key, new Set());\n }\n\n const keyListeners = listeners.get(key)!;\n keyListeners.add(listener);\n\n return () => {\n keyListeners.delete(listener);\n\n // Cleanup: remove the key entry if no more listeners\n if (keyListeners.size === 0) {\n listeners.delete(key);\n }\n };\n}\n\n/**\n * Notify all listeners subscribed to a specific key\n * This is called when setValue or removeValue is invoked\n * to synchronize all components using the same key in the same tab\n *\n * @param key - The localStorage key that was updated\n */\nexport function notifyListeners(key: string): void {\n const keyListeners = listeners.get(key);\n if (keyListeners) {\n keyListeners.forEach((listener) => listener());\n }\n}\n\n/**\n * Get the count of listeners for a key (for testing purposes)\n * @internal\n */\nexport function getListenerCount(key: string): number {\n return listeners.get(key)?.size ?? 0;\n}\n\n/**\n * Clear all listeners (for testing purposes)\n * @internal\n */\nexport function clearAllListeners(): void {\n listeners.clear();\n}\n\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED