@usefy/use-local-storage 0.0.2

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.
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Type for initial value that can be a value or a function returning a value (lazy initialization)
3
+ */
4
+ type InitialValue<T> = T | (() => T);
5
+ /**
6
+ * Options for useLocalStorage hook
7
+ */
8
+ interface UseLocalStorageOptions<T> {
9
+ /**
10
+ * Custom serializer function for converting value to string
11
+ * @default JSON.stringify
12
+ */
13
+ serializer?: (value: T) => string;
14
+ /**
15
+ * Custom deserializer function for parsing stored string to value
16
+ * @default JSON.parse
17
+ */
18
+ deserializer?: (value: string) => T;
19
+ /**
20
+ * Whether to sync value across browser tabs via storage event
21
+ * @default true
22
+ */
23
+ syncTabs?: boolean;
24
+ /**
25
+ * Callback function called when an error occurs
26
+ */
27
+ onError?: (error: Error) => void;
28
+ }
29
+ /**
30
+ * Return type for useLocalStorage hook - tuple similar to useState
31
+ */
32
+ type UseLocalStorageReturn<T> = readonly [
33
+ /** Current stored value */
34
+ T,
35
+ /** Function to update the value (same signature as useState setter) */
36
+ React.Dispatch<React.SetStateAction<T>>,
37
+ /** Function to remove the value from localStorage */
38
+ () => void
39
+ ];
40
+ /**
41
+ * A hook for persisting state in localStorage with automatic synchronization.
42
+ * Works like useState but persists the value in localStorage.
43
+ *
44
+ * @template T - The type of the stored value
45
+ * @param key - The localStorage key to store the value under
46
+ * @param initialValue - Initial value or function returning initial value (lazy initialization)
47
+ * @param options - Configuration options for serialization, sync, and error handling
48
+ * @returns A tuple of [storedValue, setValue, removeValue]
49
+ *
50
+ * @example
51
+ * ```tsx
52
+ * // Basic usage with string
53
+ * function ThemeToggle() {
54
+ * const [theme, setTheme, removeTheme] = useLocalStorage('theme', 'light');
55
+ *
56
+ * return (
57
+ * <div>
58
+ * <p>Current theme: {theme}</p>
59
+ * <button onClick={() => setTheme('dark')}>Dark</button>
60
+ * <button onClick={() => setTheme('light')}>Light</button>
61
+ * <button onClick={removeTheme}>Reset</button>
62
+ * </div>
63
+ * );
64
+ * }
65
+ * ```
66
+ *
67
+ * @example
68
+ * ```tsx
69
+ * // With object type
70
+ * interface UserSettings {
71
+ * notifications: boolean;
72
+ * language: string;
73
+ * }
74
+ *
75
+ * const [settings, setSettings] = useLocalStorage<UserSettings>('settings', {
76
+ * notifications: true,
77
+ * language: 'en',
78
+ * });
79
+ *
80
+ * // Functional update
81
+ * setSettings(prev => ({ ...prev, notifications: false }));
82
+ * ```
83
+ *
84
+ * @example
85
+ * ```tsx
86
+ * // With lazy initialization
87
+ * const [config, setConfig] = useLocalStorage('config', () => computeExpensiveDefault());
88
+ * ```
89
+ *
90
+ * @example
91
+ * ```tsx
92
+ * // With custom serializer/deserializer
93
+ * const [date, setDate] = useLocalStorage<Date>('lastVisit', new Date(), {
94
+ * serializer: (d) => d.toISOString(),
95
+ * deserializer: (s) => new Date(s),
96
+ * });
97
+ * ```
98
+ *
99
+ * @example
100
+ * ```tsx
101
+ * // With error handling
102
+ * const [value, setValue] = useLocalStorage('key', 'default', {
103
+ * onError: (error) => console.error('Storage error:', error),
104
+ * });
105
+ * ```
106
+ */
107
+ declare function useLocalStorage<T>(key: string, initialValue: InitialValue<T>, options?: UseLocalStorageOptions<T>): UseLocalStorageReturn<T>;
108
+
109
+ export { type InitialValue, type UseLocalStorageOptions, type UseLocalStorageReturn, useLocalStorage };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Type for initial value that can be a value or a function returning a value (lazy initialization)
3
+ */
4
+ type InitialValue<T> = T | (() => T);
5
+ /**
6
+ * Options for useLocalStorage hook
7
+ */
8
+ interface UseLocalStorageOptions<T> {
9
+ /**
10
+ * Custom serializer function for converting value to string
11
+ * @default JSON.stringify
12
+ */
13
+ serializer?: (value: T) => string;
14
+ /**
15
+ * Custom deserializer function for parsing stored string to value
16
+ * @default JSON.parse
17
+ */
18
+ deserializer?: (value: string) => T;
19
+ /**
20
+ * Whether to sync value across browser tabs via storage event
21
+ * @default true
22
+ */
23
+ syncTabs?: boolean;
24
+ /**
25
+ * Callback function called when an error occurs
26
+ */
27
+ onError?: (error: Error) => void;
28
+ }
29
+ /**
30
+ * Return type for useLocalStorage hook - tuple similar to useState
31
+ */
32
+ type UseLocalStorageReturn<T> = readonly [
33
+ /** Current stored value */
34
+ T,
35
+ /** Function to update the value (same signature as useState setter) */
36
+ React.Dispatch<React.SetStateAction<T>>,
37
+ /** Function to remove the value from localStorage */
38
+ () => void
39
+ ];
40
+ /**
41
+ * A hook for persisting state in localStorage with automatic synchronization.
42
+ * Works like useState but persists the value in localStorage.
43
+ *
44
+ * @template T - The type of the stored value
45
+ * @param key - The localStorage key to store the value under
46
+ * @param initialValue - Initial value or function returning initial value (lazy initialization)
47
+ * @param options - Configuration options for serialization, sync, and error handling
48
+ * @returns A tuple of [storedValue, setValue, removeValue]
49
+ *
50
+ * @example
51
+ * ```tsx
52
+ * // Basic usage with string
53
+ * function ThemeToggle() {
54
+ * const [theme, setTheme, removeTheme] = useLocalStorage('theme', 'light');
55
+ *
56
+ * return (
57
+ * <div>
58
+ * <p>Current theme: {theme}</p>
59
+ * <button onClick={() => setTheme('dark')}>Dark</button>
60
+ * <button onClick={() => setTheme('light')}>Light</button>
61
+ * <button onClick={removeTheme}>Reset</button>
62
+ * </div>
63
+ * );
64
+ * }
65
+ * ```
66
+ *
67
+ * @example
68
+ * ```tsx
69
+ * // With object type
70
+ * interface UserSettings {
71
+ * notifications: boolean;
72
+ * language: string;
73
+ * }
74
+ *
75
+ * const [settings, setSettings] = useLocalStorage<UserSettings>('settings', {
76
+ * notifications: true,
77
+ * language: 'en',
78
+ * });
79
+ *
80
+ * // Functional update
81
+ * setSettings(prev => ({ ...prev, notifications: false }));
82
+ * ```
83
+ *
84
+ * @example
85
+ * ```tsx
86
+ * // With lazy initialization
87
+ * const [config, setConfig] = useLocalStorage('config', () => computeExpensiveDefault());
88
+ * ```
89
+ *
90
+ * @example
91
+ * ```tsx
92
+ * // With custom serializer/deserializer
93
+ * const [date, setDate] = useLocalStorage<Date>('lastVisit', new Date(), {
94
+ * serializer: (d) => d.toISOString(),
95
+ * deserializer: (s) => new Date(s),
96
+ * });
97
+ * ```
98
+ *
99
+ * @example
100
+ * ```tsx
101
+ * // With error handling
102
+ * const [value, setValue] = useLocalStorage('key', 'default', {
103
+ * onError: (error) => console.error('Storage error:', error),
104
+ * });
105
+ * ```
106
+ */
107
+ declare function useLocalStorage<T>(key: string, initialValue: InitialValue<T>, options?: UseLocalStorageOptions<T>): UseLocalStorageReturn<T>;
108
+
109
+ export { type InitialValue, type UseLocalStorageOptions, type UseLocalStorageReturn, useLocalStorage };
package/dist/index.js ADDED
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ useLocalStorage: () => useLocalStorage
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/useLocalStorage.ts
28
+ var import_react = require("react");
29
+ function resolveInitialValue(initialValue) {
30
+ return typeof initialValue === "function" ? initialValue() : initialValue;
31
+ }
32
+ function useLocalStorage(key, initialValue, options = {}) {
33
+ const {
34
+ serializer = JSON.stringify,
35
+ deserializer = JSON.parse,
36
+ syncTabs = true,
37
+ onError
38
+ } = options;
39
+ const serializerRef = (0, import_react.useRef)(serializer);
40
+ const deserializerRef = (0, import_react.useRef)(deserializer);
41
+ const onErrorRef = (0, import_react.useRef)(onError);
42
+ serializerRef.current = serializer;
43
+ deserializerRef.current = deserializer;
44
+ onErrorRef.current = onError;
45
+ const initialValueRef = (0, import_react.useRef)(initialValue);
46
+ initialValueRef.current = initialValue;
47
+ const isClient = typeof window !== "undefined";
48
+ const [storedValue, setStoredValue] = (0, import_react.useState)(() => {
49
+ if (!isClient) {
50
+ return resolveInitialValue(initialValue);
51
+ }
52
+ try {
53
+ const item = window.localStorage.getItem(key);
54
+ if (item !== null) {
55
+ return deserializerRef.current(item);
56
+ }
57
+ return resolveInitialValue(initialValue);
58
+ } catch (error) {
59
+ onErrorRef.current?.(error);
60
+ return resolveInitialValue(initialValue);
61
+ }
62
+ });
63
+ const storedValueRef = (0, import_react.useRef)(storedValue);
64
+ storedValueRef.current = storedValue;
65
+ const setValue = (0, import_react.useCallback)(
66
+ (value) => {
67
+ try {
68
+ const currentValue = storedValueRef.current;
69
+ const valueToStore = value instanceof Function ? value(currentValue) : value;
70
+ setStoredValue(valueToStore);
71
+ if (typeof window !== "undefined") {
72
+ window.localStorage.setItem(
73
+ key,
74
+ serializerRef.current(valueToStore)
75
+ );
76
+ }
77
+ } catch (error) {
78
+ onErrorRef.current?.(error);
79
+ }
80
+ },
81
+ [key]
82
+ );
83
+ const removeValue = (0, import_react.useCallback)(() => {
84
+ try {
85
+ const initial = resolveInitialValue(initialValueRef.current);
86
+ setStoredValue(initial);
87
+ if (typeof window !== "undefined") {
88
+ window.localStorage.removeItem(key);
89
+ }
90
+ } catch (error) {
91
+ onErrorRef.current?.(error);
92
+ }
93
+ }, [key]);
94
+ (0, import_react.useEffect)(() => {
95
+ if (!isClient || !syncTabs) {
96
+ return;
97
+ }
98
+ const handleStorageChange = (event) => {
99
+ if (event.key !== key) {
100
+ return;
101
+ }
102
+ if (event.newValue !== null) {
103
+ try {
104
+ setStoredValue(deserializerRef.current(event.newValue));
105
+ } catch {
106
+ setStoredValue(resolveInitialValue(initialValueRef.current));
107
+ }
108
+ } else {
109
+ setStoredValue(resolveInitialValue(initialValueRef.current));
110
+ }
111
+ };
112
+ window.addEventListener("storage", handleStorageChange);
113
+ return () => window.removeEventListener("storage", handleStorageChange);
114
+ }, [key, syncTabs, isClient]);
115
+ (0, import_react.useEffect)(() => {
116
+ if (!isClient) {
117
+ return;
118
+ }
119
+ try {
120
+ const item = window.localStorage.getItem(key);
121
+ if (item !== null) {
122
+ setStoredValue(deserializerRef.current(item));
123
+ } else {
124
+ setStoredValue(resolveInitialValue(initialValueRef.current));
125
+ }
126
+ } catch {
127
+ setStoredValue(resolveInitialValue(initialValueRef.current));
128
+ }
129
+ }, [key]);
130
+ return [storedValue, setValue, removeValue];
131
+ }
132
+ // Annotate the CommonJS export names for ESM import in node:
133
+ 0 && (module.exports = {
134
+ useLocalStorage
135
+ });
136
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/useLocalStorage.ts"],"sourcesContent":["export {\r\n useLocalStorage,\r\n type UseLocalStorageOptions,\r\n type UseLocalStorageReturn,\r\n type InitialValue,\r\n} from \"./useLocalStorage\";\r\n","import { useCallback, useEffect, useRef, useState } from \"react\";\r\n\r\n/**\r\n * Type for initial value that can be a value or a function returning a value (lazy initialization)\r\n */\r\nexport type InitialValue<T> = T | (() => T);\r\n\r\n/**\r\n * Options for useLocalStorage hook\r\n */\r\nexport interface UseLocalStorageOptions<T> {\r\n /**\r\n * Custom serializer function for converting value to string\r\n * @default JSON.stringify\r\n */\r\n serializer?: (value: T) => string;\r\n /**\r\n * Custom deserializer function for parsing stored string to value\r\n * @default JSON.parse\r\n */\r\n deserializer?: (value: string) => T;\r\n /**\r\n * Whether to sync value across browser tabs via storage event\r\n * @default true\r\n */\r\n syncTabs?: boolean;\r\n /**\r\n * Callback function called when an error occurs\r\n */\r\n onError?: (error: Error) => void;\r\n}\r\n\r\n/**\r\n * Return type for useLocalStorage hook - tuple similar to useState\r\n */\r\nexport type UseLocalStorageReturn<T> = readonly [\r\n /** Current stored value */\r\n T,\r\n /** Function to update the value (same signature as useState setter) */\r\n React.Dispatch<React.SetStateAction<T>>,\r\n /** Function to remove the value from localStorage */\r\n () => void\r\n];\r\n\r\n/**\r\n * Helper function to resolve initial value (supports lazy initialization)\r\n */\r\nfunction resolveInitialValue<T>(initialValue: InitialValue<T>): T {\r\n return typeof initialValue === \"function\"\r\n ? (initialValue as () => T)()\r\n : initialValue;\r\n}\r\n\r\n/**\r\n * A hook for persisting state in localStorage with automatic synchronization.\r\n * Works like useState but persists the value in localStorage.\r\n *\r\n * @template T - The type of the stored value\r\n * @param key - The localStorage key to store the value under\r\n * @param initialValue - Initial value or function returning initial value (lazy initialization)\r\n * @param options - Configuration options for serialization, sync, and error handling\r\n * @returns A tuple of [storedValue, setValue, removeValue]\r\n *\r\n * @example\r\n * ```tsx\r\n * // Basic usage with string\r\n * function ThemeToggle() {\r\n * const [theme, setTheme, removeTheme] = useLocalStorage('theme', 'light');\r\n *\r\n * return (\r\n * <div>\r\n * <p>Current theme: {theme}</p>\r\n * <button onClick={() => setTheme('dark')}>Dark</button>\r\n * <button onClick={() => setTheme('light')}>Light</button>\r\n * <button onClick={removeTheme}>Reset</button>\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n *\r\n * @example\r\n * ```tsx\r\n * // With object type\r\n * interface UserSettings {\r\n * notifications: boolean;\r\n * language: string;\r\n * }\r\n *\r\n * const [settings, setSettings] = useLocalStorage<UserSettings>('settings', {\r\n * notifications: true,\r\n * language: 'en',\r\n * });\r\n *\r\n * // Functional update\r\n * setSettings(prev => ({ ...prev, notifications: false }));\r\n * ```\r\n *\r\n * @example\r\n * ```tsx\r\n * // With lazy initialization\r\n * const [config, setConfig] = useLocalStorage('config', () => computeExpensiveDefault());\r\n * ```\r\n *\r\n * @example\r\n * ```tsx\r\n * // With custom serializer/deserializer\r\n * const [date, setDate] = useLocalStorage<Date>('lastVisit', new Date(), {\r\n * serializer: (d) => d.toISOString(),\r\n * deserializer: (s) => new Date(s),\r\n * });\r\n * ```\r\n *\r\n * @example\r\n * ```tsx\r\n * // With error handling\r\n * const [value, setValue] = useLocalStorage('key', 'default', {\r\n * onError: (error) => console.error('Storage error:', error),\r\n * });\r\n * ```\r\n */\r\nexport function useLocalStorage<T>(\r\n key: string,\r\n initialValue: InitialValue<T>,\r\n options: UseLocalStorageOptions<T> = {}\r\n): UseLocalStorageReturn<T> {\r\n const {\r\n serializer = JSON.stringify,\r\n deserializer = JSON.parse,\r\n syncTabs = true,\r\n onError,\r\n } = options;\r\n\r\n // Store options in refs for stable references and access to latest values\r\n const serializerRef = useRef(serializer);\r\n const deserializerRef = useRef(deserializer);\r\n const onErrorRef = useRef(onError);\r\n serializerRef.current = serializer;\r\n deserializerRef.current = deserializer;\r\n onErrorRef.current = onError;\r\n\r\n // Store initialValue in ref for use in removeValue and storage event handler\r\n const initialValueRef = useRef(initialValue);\r\n initialValueRef.current = initialValue;\r\n\r\n // SSR check - check once at module level for consistency\r\n const isClient = typeof window !== \"undefined\";\r\n\r\n // Lazy initialization with localStorage read\r\n const [storedValue, setStoredValue] = useState<T>(() => {\r\n if (!isClient) {\r\n return resolveInitialValue(initialValue);\r\n }\r\n\r\n try {\r\n const item = window.localStorage.getItem(key);\r\n if (item !== null) {\r\n return deserializerRef.current(item);\r\n }\r\n return resolveInitialValue(initialValue);\r\n } catch (error) {\r\n onErrorRef.current?.(error as Error);\r\n return resolveInitialValue(initialValue);\r\n }\r\n });\r\n\r\n // Store current value in ref for stable setValue reference\r\n const storedValueRef = useRef<T>(storedValue);\r\n storedValueRef.current = storedValue;\r\n\r\n // setValue - stable reference (only depends on key)\r\n const setValue = useCallback<React.Dispatch<React.SetStateAction<T>>>(\r\n (value) => {\r\n try {\r\n const currentValue = storedValueRef.current;\r\n const valueToStore =\r\n value instanceof Function ? value(currentValue) : value;\r\n\r\n setStoredValue(valueToStore);\r\n\r\n if (typeof window !== \"undefined\") {\r\n window.localStorage.setItem(\r\n key,\r\n serializerRef.current(valueToStore)\r\n );\r\n }\r\n } catch (error) {\r\n onErrorRef.current?.(error as Error);\r\n }\r\n },\r\n [key]\r\n );\r\n\r\n // removeValue - stable reference\r\n const removeValue = useCallback(() => {\r\n try {\r\n const initial = resolveInitialValue(initialValueRef.current);\r\n setStoredValue(initial);\r\n\r\n if (typeof window !== \"undefined\") {\r\n window.localStorage.removeItem(key);\r\n }\r\n } catch (error) {\r\n onErrorRef.current?.(error as Error);\r\n }\r\n }, [key]);\r\n\r\n // Cross-tab synchronization via storage event\r\n useEffect(() => {\r\n if (!isClient || !syncTabs) {\r\n return;\r\n }\r\n\r\n const handleStorageChange = (event: StorageEvent) => {\r\n if (event.key !== key) {\r\n return;\r\n }\r\n\r\n if (event.newValue !== null) {\r\n try {\r\n setStoredValue(deserializerRef.current(event.newValue));\r\n } catch {\r\n // If parsing fails, reset to initial value\r\n setStoredValue(resolveInitialValue(initialValueRef.current));\r\n }\r\n } else {\r\n // Key was removed in another tab\r\n setStoredValue(resolveInitialValue(initialValueRef.current));\r\n }\r\n };\r\n\r\n window.addEventListener(\"storage\", handleStorageChange);\r\n return () => window.removeEventListener(\"storage\", handleStorageChange);\r\n }, [key, syncTabs, isClient]);\r\n\r\n // Re-read value when key changes\r\n useEffect(() => {\r\n if (!isClient) {\r\n return;\r\n }\r\n\r\n try {\r\n const item = window.localStorage.getItem(key);\r\n if (item !== null) {\r\n setStoredValue(deserializerRef.current(item));\r\n } else {\r\n setStoredValue(resolveInitialValue(initialValueRef.current));\r\n }\r\n } catch {\r\n setStoredValue(resolveInitialValue(initialValueRef.current));\r\n }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [key]);\r\n\r\n return [storedValue, setValue, removeValue] as const;\r\n}\r\n"],"mappings":"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","names":[]}
package/dist/index.mjs ADDED
@@ -0,0 +1,109 @@
1
+ // src/useLocalStorage.ts
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ function resolveInitialValue(initialValue) {
4
+ return typeof initialValue === "function" ? initialValue() : initialValue;
5
+ }
6
+ function useLocalStorage(key, initialValue, options = {}) {
7
+ const {
8
+ serializer = JSON.stringify,
9
+ deserializer = JSON.parse,
10
+ syncTabs = true,
11
+ onError
12
+ } = options;
13
+ const serializerRef = useRef(serializer);
14
+ const deserializerRef = useRef(deserializer);
15
+ const onErrorRef = useRef(onError);
16
+ serializerRef.current = serializer;
17
+ deserializerRef.current = deserializer;
18
+ onErrorRef.current = onError;
19
+ const initialValueRef = useRef(initialValue);
20
+ initialValueRef.current = initialValue;
21
+ const isClient = typeof window !== "undefined";
22
+ const [storedValue, setStoredValue] = useState(() => {
23
+ if (!isClient) {
24
+ return resolveInitialValue(initialValue);
25
+ }
26
+ try {
27
+ const item = window.localStorage.getItem(key);
28
+ if (item !== null) {
29
+ return deserializerRef.current(item);
30
+ }
31
+ return resolveInitialValue(initialValue);
32
+ } catch (error) {
33
+ onErrorRef.current?.(error);
34
+ return resolveInitialValue(initialValue);
35
+ }
36
+ });
37
+ const storedValueRef = useRef(storedValue);
38
+ storedValueRef.current = storedValue;
39
+ const setValue = useCallback(
40
+ (value) => {
41
+ try {
42
+ const currentValue = storedValueRef.current;
43
+ const valueToStore = value instanceof Function ? value(currentValue) : value;
44
+ setStoredValue(valueToStore);
45
+ if (typeof window !== "undefined") {
46
+ window.localStorage.setItem(
47
+ key,
48
+ serializerRef.current(valueToStore)
49
+ );
50
+ }
51
+ } catch (error) {
52
+ onErrorRef.current?.(error);
53
+ }
54
+ },
55
+ [key]
56
+ );
57
+ const removeValue = useCallback(() => {
58
+ try {
59
+ const initial = resolveInitialValue(initialValueRef.current);
60
+ setStoredValue(initial);
61
+ if (typeof window !== "undefined") {
62
+ window.localStorage.removeItem(key);
63
+ }
64
+ } catch (error) {
65
+ onErrorRef.current?.(error);
66
+ }
67
+ }, [key]);
68
+ useEffect(() => {
69
+ if (!isClient || !syncTabs) {
70
+ return;
71
+ }
72
+ const handleStorageChange = (event) => {
73
+ if (event.key !== key) {
74
+ return;
75
+ }
76
+ if (event.newValue !== null) {
77
+ try {
78
+ setStoredValue(deserializerRef.current(event.newValue));
79
+ } catch {
80
+ setStoredValue(resolveInitialValue(initialValueRef.current));
81
+ }
82
+ } else {
83
+ setStoredValue(resolveInitialValue(initialValueRef.current));
84
+ }
85
+ };
86
+ window.addEventListener("storage", handleStorageChange);
87
+ return () => window.removeEventListener("storage", handleStorageChange);
88
+ }, [key, syncTabs, isClient]);
89
+ useEffect(() => {
90
+ if (!isClient) {
91
+ return;
92
+ }
93
+ try {
94
+ const item = window.localStorage.getItem(key);
95
+ if (item !== null) {
96
+ setStoredValue(deserializerRef.current(item));
97
+ } else {
98
+ setStoredValue(resolveInitialValue(initialValueRef.current));
99
+ }
100
+ } catch {
101
+ setStoredValue(resolveInitialValue(initialValueRef.current));
102
+ }
103
+ }, [key]);
104
+ return [storedValue, setValue, removeValue];
105
+ }
106
+ export {
107
+ useLocalStorage
108
+ };
109
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/useLocalStorage.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\r\n\r\n/**\r\n * Type for initial value that can be a value or a function returning a value (lazy initialization)\r\n */\r\nexport type InitialValue<T> = T | (() => T);\r\n\r\n/**\r\n * Options for useLocalStorage hook\r\n */\r\nexport interface UseLocalStorageOptions<T> {\r\n /**\r\n * Custom serializer function for converting value to string\r\n * @default JSON.stringify\r\n */\r\n serializer?: (value: T) => string;\r\n /**\r\n * Custom deserializer function for parsing stored string to value\r\n * @default JSON.parse\r\n */\r\n deserializer?: (value: string) => T;\r\n /**\r\n * Whether to sync value across browser tabs via storage event\r\n * @default true\r\n */\r\n syncTabs?: boolean;\r\n /**\r\n * Callback function called when an error occurs\r\n */\r\n onError?: (error: Error) => void;\r\n}\r\n\r\n/**\r\n * Return type for useLocalStorage hook - tuple similar to useState\r\n */\r\nexport type UseLocalStorageReturn<T> = readonly [\r\n /** Current stored value */\r\n T,\r\n /** Function to update the value (same signature as useState setter) */\r\n React.Dispatch<React.SetStateAction<T>>,\r\n /** Function to remove the value from localStorage */\r\n () => void\r\n];\r\n\r\n/**\r\n * Helper function to resolve initial value (supports lazy initialization)\r\n */\r\nfunction resolveInitialValue<T>(initialValue: InitialValue<T>): T {\r\n return typeof initialValue === \"function\"\r\n ? (initialValue as () => T)()\r\n : initialValue;\r\n}\r\n\r\n/**\r\n * A hook for persisting state in localStorage with automatic synchronization.\r\n * Works like useState but persists the value in localStorage.\r\n *\r\n * @template T - The type of the stored value\r\n * @param key - The localStorage key to store the value under\r\n * @param initialValue - Initial value or function returning initial value (lazy initialization)\r\n * @param options - Configuration options for serialization, sync, and error handling\r\n * @returns A tuple of [storedValue, setValue, removeValue]\r\n *\r\n * @example\r\n * ```tsx\r\n * // Basic usage with string\r\n * function ThemeToggle() {\r\n * const [theme, setTheme, removeTheme] = useLocalStorage('theme', 'light');\r\n *\r\n * return (\r\n * <div>\r\n * <p>Current theme: {theme}</p>\r\n * <button onClick={() => setTheme('dark')}>Dark</button>\r\n * <button onClick={() => setTheme('light')}>Light</button>\r\n * <button onClick={removeTheme}>Reset</button>\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n *\r\n * @example\r\n * ```tsx\r\n * // With object type\r\n * interface UserSettings {\r\n * notifications: boolean;\r\n * language: string;\r\n * }\r\n *\r\n * const [settings, setSettings] = useLocalStorage<UserSettings>('settings', {\r\n * notifications: true,\r\n * language: 'en',\r\n * });\r\n *\r\n * // Functional update\r\n * setSettings(prev => ({ ...prev, notifications: false }));\r\n * ```\r\n *\r\n * @example\r\n * ```tsx\r\n * // With lazy initialization\r\n * const [config, setConfig] = useLocalStorage('config', () => computeExpensiveDefault());\r\n * ```\r\n *\r\n * @example\r\n * ```tsx\r\n * // With custom serializer/deserializer\r\n * const [date, setDate] = useLocalStorage<Date>('lastVisit', new Date(), {\r\n * serializer: (d) => d.toISOString(),\r\n * deserializer: (s) => new Date(s),\r\n * });\r\n * ```\r\n *\r\n * @example\r\n * ```tsx\r\n * // With error handling\r\n * const [value, setValue] = useLocalStorage('key', 'default', {\r\n * onError: (error) => console.error('Storage error:', error),\r\n * });\r\n * ```\r\n */\r\nexport function useLocalStorage<T>(\r\n key: string,\r\n initialValue: InitialValue<T>,\r\n options: UseLocalStorageOptions<T> = {}\r\n): UseLocalStorageReturn<T> {\r\n const {\r\n serializer = JSON.stringify,\r\n deserializer = JSON.parse,\r\n syncTabs = true,\r\n onError,\r\n } = options;\r\n\r\n // Store options in refs for stable references and access to latest values\r\n const serializerRef = useRef(serializer);\r\n const deserializerRef = useRef(deserializer);\r\n const onErrorRef = useRef(onError);\r\n serializerRef.current = serializer;\r\n deserializerRef.current = deserializer;\r\n onErrorRef.current = onError;\r\n\r\n // Store initialValue in ref for use in removeValue and storage event handler\r\n const initialValueRef = useRef(initialValue);\r\n initialValueRef.current = initialValue;\r\n\r\n // SSR check - check once at module level for consistency\r\n const isClient = typeof window !== \"undefined\";\r\n\r\n // Lazy initialization with localStorage read\r\n const [storedValue, setStoredValue] = useState<T>(() => {\r\n if (!isClient) {\r\n return resolveInitialValue(initialValue);\r\n }\r\n\r\n try {\r\n const item = window.localStorage.getItem(key);\r\n if (item !== null) {\r\n return deserializerRef.current(item);\r\n }\r\n return resolveInitialValue(initialValue);\r\n } catch (error) {\r\n onErrorRef.current?.(error as Error);\r\n return resolveInitialValue(initialValue);\r\n }\r\n });\r\n\r\n // Store current value in ref for stable setValue reference\r\n const storedValueRef = useRef<T>(storedValue);\r\n storedValueRef.current = storedValue;\r\n\r\n // setValue - stable reference (only depends on key)\r\n const setValue = useCallback<React.Dispatch<React.SetStateAction<T>>>(\r\n (value) => {\r\n try {\r\n const currentValue = storedValueRef.current;\r\n const valueToStore =\r\n value instanceof Function ? value(currentValue) : value;\r\n\r\n setStoredValue(valueToStore);\r\n\r\n if (typeof window !== \"undefined\") {\r\n window.localStorage.setItem(\r\n key,\r\n serializerRef.current(valueToStore)\r\n );\r\n }\r\n } catch (error) {\r\n onErrorRef.current?.(error as Error);\r\n }\r\n },\r\n [key]\r\n );\r\n\r\n // removeValue - stable reference\r\n const removeValue = useCallback(() => {\r\n try {\r\n const initial = resolveInitialValue(initialValueRef.current);\r\n setStoredValue(initial);\r\n\r\n if (typeof window !== \"undefined\") {\r\n window.localStorage.removeItem(key);\r\n }\r\n } catch (error) {\r\n onErrorRef.current?.(error as Error);\r\n }\r\n }, [key]);\r\n\r\n // Cross-tab synchronization via storage event\r\n useEffect(() => {\r\n if (!isClient || !syncTabs) {\r\n return;\r\n }\r\n\r\n const handleStorageChange = (event: StorageEvent) => {\r\n if (event.key !== key) {\r\n return;\r\n }\r\n\r\n if (event.newValue !== null) {\r\n try {\r\n setStoredValue(deserializerRef.current(event.newValue));\r\n } catch {\r\n // If parsing fails, reset to initial value\r\n setStoredValue(resolveInitialValue(initialValueRef.current));\r\n }\r\n } else {\r\n // Key was removed in another tab\r\n setStoredValue(resolveInitialValue(initialValueRef.current));\r\n }\r\n };\r\n\r\n window.addEventListener(\"storage\", handleStorageChange);\r\n return () => window.removeEventListener(\"storage\", handleStorageChange);\r\n }, [key, syncTabs, isClient]);\r\n\r\n // Re-read value when key changes\r\n useEffect(() => {\r\n if (!isClient) {\r\n return;\r\n }\r\n\r\n try {\r\n const item = window.localStorage.getItem(key);\r\n if (item !== null) {\r\n setStoredValue(deserializerRef.current(item));\r\n } else {\r\n setStoredValue(resolveInitialValue(initialValueRef.current));\r\n }\r\n } catch {\r\n setStoredValue(resolveInitialValue(initialValueRef.current));\r\n }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [key]);\r\n\r\n return [storedValue, setValue, removeValue] as const;\r\n}\r\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@usefy/use-local-storage",
3
+ "version": "0.0.2",
4
+ "description": "A React hook for persisting state in localStorage with automatic synchronization",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "sideEffects": false,
19
+ "peerDependencies": {
20
+ "react": "^18.0.0 || ^19.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "@testing-library/jest-dom": "^6.9.1",
24
+ "@testing-library/react": "^16.3.1",
25
+ "@testing-library/user-event": "^14.6.1",
26
+ "@types/react": "^19.0.0",
27
+ "jsdom": "^27.3.0",
28
+ "react": "^19.0.0",
29
+ "rimraf": "^6.0.1",
30
+ "tsup": "^8.0.0",
31
+ "typescript": "^5.0.0",
32
+ "vitest": "^4.0.16"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/geon0529/usefy.git",
40
+ "directory": "packages/use-local-storage"
41
+ },
42
+ "license": "MIT",
43
+ "keywords": [
44
+ "react",
45
+ "hooks",
46
+ "localStorage",
47
+ "storage",
48
+ "state",
49
+ "persistence"
50
+ ],
51
+ "scripts": {
52
+ "build": "tsup",
53
+ "dev": "tsup --watch",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest",
56
+ "typecheck": "tsc --noEmit",
57
+ "clean": "rimraf dist"
58
+ }
59
+ }