use-storage-persisted-state 1.0.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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+
2
+ MIT License
3
+
4
+ Copyright (c) 2026 mikkoha
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # use-storage-persisted-state
2
+
3
+ A robust, type-safe React hook for persisting state backed by `localStorage`, `sessionStorage`, or memory.
4
+
5
+ `useStoragePersistedState` works like `useState`, but it automatically persists your state to the browser and keeps it synchronized across all components, tabs, and even direct localStorage changes, or manual changes in DevTools.
6
+
7
+ ## Features (Why another storage hook?)
8
+
9
+ - **Type safety**: Full TypeScript type inference and safety.
10
+ - **Sync between components**: Keeps state synchronized across all components using the same key.
11
+ - **Cross-tab sync**: Automatically synchronizes state across tabs (using native `StorageEvent`).
12
+ - **External change detection**: Detects changes made directly to storage (e.g., via DevTools or `window.localStorage.setItem`) using (optional) polling.
13
+ - **SSR ready**: Safe for Server-Side Rendering (e.g., Next.js) using proper hydration techniques (React `useSyncExternalStore` with a shim for React 16.8+ support).
14
+ - **Custom serialization**: Supports custom serializer implementation for advanced use cases like data schema migration.
15
+ - **Graceful error handling**: Automatically falls back to in-memory storage if `QuotaExceededError` occurs or storage is unavailable.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install use-storage-persisted-state
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ### 1. Basic usage
26
+
27
+ ```tsx
28
+ import { useStoragePersistedState } from "use-storage-persisted-state";
29
+
30
+ function Counter() {
31
+ const [count, setCount] = useStoragePersistedState("count", 0);
32
+
33
+ return (
34
+ <button onClick={() => setCount((prev) => prev + 1)}>Count: {count}</button>
35
+ );
36
+ }
37
+ ```
38
+
39
+ ### 2. Basic sync example
40
+
41
+ Any component using the same key will stay in sync, even across different tabs. The state survives page reloads, because it is stored in `localStorage` (default).
42
+
43
+ ```tsx
44
+ import { useStoragePersistedState } from "use-storage-persisted-state";
45
+
46
+ function ComponentA() {
47
+ const [username, setUsername] = useStoragePersistedState(
48
+ "user_name",
49
+ "Guest",
50
+ );
51
+ return (
52
+ <input value={username} onChange={(e) => setUsername(e.target.value)} />
53
+ );
54
+ }
55
+
56
+ function ComponentB() {
57
+ const [username] = useStoragePersistedState("user_name", "Guest");
58
+ return <p>Hello, {username}!</p>;
59
+ }
60
+ ```
61
+
62
+ ### 3. Explicit codec (undefined default value)
63
+
64
+ If your default value is `undefined` or `null`, you must provide an explicit codec so the hook knows how to serialize/deserialize the data.
65
+
66
+ ```tsx
67
+ import {
68
+ useStoragePersistedState,
69
+ StringCodec,
70
+ } from "use-storage-persisted-state";
71
+
72
+ function FavoriteColor() {
73
+ // We use StringCodec explicitly since defaultValue is undefined and Codec cannot be inferred.
74
+ const [color, setColor] = useStoragePersistedState<string | undefined>(
75
+ "favorite_color",
76
+ undefined,
77
+ { codec: StringCodec },
78
+ );
79
+
80
+ return (
81
+ <input
82
+ value={color ?? ""}
83
+ onChange={(e) => setColor(e.target.value || undefined)}
84
+ placeholder="Enter your favorite color"
85
+ />
86
+ );
87
+ }
88
+ ```
89
+
90
+ ```tsx
91
+ import {
92
+ useStoragePersistedState,
93
+ JsonCodec,
94
+ } from "use-storage-persisted-state";
95
+
96
+ function UserProfile() {
97
+ // We use JsonCodec explicitly since Codec inference from 'null' is ambiguous (could be string | null, number | null, etc.)
98
+ const [user, setUser] = useStoragePersistedState<{ name: string } | null>(
99
+ "user_profile",
100
+ null,
101
+ { codec: JsonCodec },
102
+ );
103
+
104
+ if (!user)
105
+ return <button onClick={() => setUser({ name: "Alice" })}>Login</button>;
106
+
107
+ return <div>Welcome, {user.name}</div>;
108
+ }
109
+ ```
110
+
111
+ By default, the codec is inferred from the type of `defaultValue` if possible.
112
+
113
+ - If `defaultValue` is a primitive type (string, number, boolean), the value is stored as a simple string (with StringCodec, NumberCodec, or BooleanCodec respectively).
114
+ - If `defaultValue` is an object or array, a built-in `JsonCodec` is used by default.
115
+ - There is nothing magical about codecs; they are just objects with `encode` (e.g., `JSON.stringify`) and `decode` (e.g., `JSON.parse`) methods. You can provide your own codec for custom serialization logic.
116
+
117
+ ### 4. Read and write outside React
118
+
119
+ You can read and write values without using the hook. These utilities still parse via codecs and notify active hooks using the same key. You can, of course, also use window.localStorage/sessionStorage directly, but then you have to handle serialization and hook notifications yourself (if you're not using polling or want immediate updates).
120
+
121
+ ```tsx
122
+ import {
123
+ readStoragePersistedState,
124
+ setStoragePersistedState,
125
+ JsonCodec,
126
+ } from "use-storage-persisted-state";
127
+
128
+ // Read a number value with inferred NumberCodec.
129
+ const count = readStoragePersistedState("count", 0);
130
+
131
+ // Explicit codec is required since the default value is null.
132
+ const user = readStoragePersistedState<{ name: string } | null>(
133
+ "user_profile",
134
+ null,
135
+ { codec: JsonCodec },
136
+ );
137
+
138
+ // Write an object value with inferred JsonCodec.
139
+ setStoragePersistedState("user_profile", { name: "Alice" });
140
+ ```
141
+
142
+ ## Advanced usage
143
+
144
+ ### Data schema migration with custom codec
145
+
146
+ You can handle schema migrations (e.g., renaming fields) by creating a custom codec.
147
+
148
+ ```tsx
149
+ import {
150
+ useStoragePersistedState,
151
+ Codec,
152
+ JsonCodec,
153
+ } from "use-storage-persisted-state";
154
+
155
+ interface OldSettings {
156
+ darkMode: boolean;
157
+ }
158
+
159
+ interface NewSettings {
160
+ theme: "dark" | "light";
161
+ }
162
+
163
+ const SettingsCodec: Codec<NewSettings> = {
164
+ encode: (value) => JSON.stringify(value),
165
+ decode: (value) => {
166
+ if (value === null) return { theme: "light" };
167
+
168
+ try {
169
+ const parsed = JSON.parse(value);
170
+
171
+ // Migration logic: convert old boolean to new string enum
172
+ if ("darkMode" in parsed) {
173
+ return { theme: parsed.darkMode ? "dark" : "light" };
174
+ }
175
+
176
+ return parsed;
177
+ } catch {
178
+ return { theme: "light" };
179
+ }
180
+ },
181
+ };
182
+
183
+ function Settings() {
184
+ const [settings, setSettings] = useStoragePersistedState<NewSettings>(
185
+ "app_settings",
186
+ { theme: "light" },
187
+ { codec: SettingsCodec },
188
+ );
189
+
190
+ return <div>Current Theme: {settings.theme}</div>;
191
+ }
192
+ ```
193
+
194
+ ## Options
195
+
196
+ `useStoragePersistedState(key, defaultValue, options)`
197
+
198
+ - `key: string` - The storage key to be used with `localStorage`, `sessionStorage`, or `memory` storage.
199
+ - `defaultValue: T` - The default value to use if there is no value in storage. Note: this is not just the initial value; it is returned whenever the stored value is missing (e.g., after removal or a read error).
200
+ - `options?: StoragePersistedStateOptions<T>` - Optional configuration object. See table below.
201
+
202
+ | Option | Type | Default | Description |
203
+ | ------------------- | ---------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
204
+ | `codec` | `Codec<T>` | Inferred | Defines how to encode/decode values. Required if `defaultValue` is `null`/`undefined`. |
205
+ | `storageType` | `'localStorage'` \| `'sessionStorage'` \| `'memory'` | `'localStorage'` | Which storage backend to use. `memory` is a simple in-memory storage that does not persist across reloads. |
206
+ | `crossTabSync` | `boolean` | `true` | Enables syncing between tabs via listening to native `StorageEvent`. |
207
+ | `pollingIntervalMs` | `number` \| `null` | `2000` | Polling interval (milliseconds) to detect changes made outside this hook (e.g. devtools). Set `null` to disable polling. |
208
+
209
+ ## FAQ
210
+
211
+ ### How is `QuotaExceededError` handled?
212
+
213
+ If `localStorage` or `sessionStorage` is full, writing to it will typically throw a `QuotaExceededError`. This library handles this gracefully by catching the error and automatically falling back to an in-memory storage for that specific key. This means your application won't crash, and the state will persist for the session (until page reload), even if it couldn't be persisted.
214
+
215
+ ### How is this different from other storage hooks?
216
+
217
+ This package shares similarities with, for example:
218
+
219
+ - `use-storage-state`
220
+ - `usehooks-ts` (`useLocalStorage`)
221
+ - `use-local-storage-state`
222
+
223
+ Key differences include:
224
+
225
+ - built-in or custom serialize/deserialize support that saves by default primitive types as simple strings, and objects and arrays as JSON
226
+ - automatic in-memory fallback (or, can be used as a memory-only synced state hook)
227
+ - robust sync behavior with optional polling for catching all external changes to underlying storage
228
+ - full TypeScript type inference and safety
229
+ - SSR ready with proper hydration using `useSyncExternalStore` (with React 16.8+ support via shim)
230
+ - handles edge-cases like `QuotaExceededError`, and other storage unavailability
231
+ - provides read/write utilities for use where hooks cannot be used, while maintaining sync and serialization
232
+
233
+ ### How does the hook handle null and undefined values?
234
+
235
+ When the state is set to `null` or `undefined`, the hook will remove the corresponding item from the underlying storage (`localStorage`/`sessionStorage`). This means that subsequent reads will return the `defaultValue` provided to the hook until an explicit value is set.
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Defines how to serialize/deserialize a value from localStorage.
3
+ * null means the key doesn't exist.
4
+ */
5
+ interface Codec<T> {
6
+ encode: (value: T) => string | null;
7
+ decode: (value: string | null) => T;
8
+ }
9
+ /**
10
+ * A robust JSON codec that handles parsing errors gracefully.
11
+ * Works with objects, arrays, and other JSON-serializable values.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const [user, setUser] = useStoragePersistedState<User | null>(
16
+ * "user",
17
+ * null,
18
+ * { codec: JsonCodec }
19
+ * );
20
+ * ```
21
+ */
22
+ declare const JsonCodec: Codec<unknown>;
23
+ /**
24
+ * Codec for string values. Stores strings as-is without any transformation.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const [name, setName] = useStoragePersistedState("name", "Guest");
29
+ * // Automatically uses StringCodec because defaultValue is a string
30
+ * ```
31
+ */
32
+ declare const StringCodec: Codec<string | null>;
33
+ /**
34
+ * Codec for boolean values. Stores as "true" or "false" strings.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const [isDark, setIsDark] = useStoragePersistedState("darkMode", false);
39
+ * // Automatically uses BooleanCodec because defaultValue is a boolean
40
+ * ```
41
+ */
42
+ declare const BooleanCodec: Codec<boolean>;
43
+ /**
44
+ * Codec for number values. Handles NaN, Infinity, and -Infinity correctly.
45
+ * Returns null for unparseable values (e.g., empty string, invalid number string).
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * const [count, setCount] = useStoragePersistedState("count", 0);
50
+ * // Automatically uses NumberCodec because defaultValue is a number
51
+ * ```
52
+ */
53
+ declare const NumberCodec: Codec<number | null>;
54
+ /**
55
+ * Infers the appropriate codec based on the default value's type.
56
+ * Used internally when the user doesn't provide an explicit codec.
57
+ *
58
+ * - `boolean` → BooleanCodec
59
+ * - `number` → NumberCodec
60
+ * - `string` → StringCodec
61
+ * - `object`, `array`, `undefined`, `null` → JsonCodec
62
+ *
63
+ * @param defaultValue - The default value to infer the codec from
64
+ * @returns The inferred codec for the given value type
65
+ */
66
+ declare function inferCodec<T>(defaultValue: T): Codec<T>;
67
+
68
+ type StorageType = "localStorage" | "sessionStorage" | "memory";
69
+ /**
70
+ * Options for the useStoragePersistedState hook
71
+ */
72
+ interface StoragePersistedStateOptions<T> {
73
+ /**
74
+ * Explicit codec for when defaultValue is null or undefined, for complex types, or special cases (e.g., data migration on read).
75
+ * If not provided, codec is inferred from defaultValue type.
76
+ */
77
+ codec?: Codec<T>;
78
+ /**
79
+ * Storage type to use: 'localStorage' (default), 'sessionStorage', or 'memory'.
80
+ */
81
+ storageType?: StorageType;
82
+ /**
83
+ * Enable cross-tab synchronization via the StorageEvent. Note: disabling this will not stop other tabs from updating localStorage, but this hook will not automatically respond to those changes.
84
+ * defaults to true.
85
+ */
86
+ crossTabSync?: boolean;
87
+ /**
88
+ * Polling interval in milliseconds for detecting storage changes made outside of React (e.g., DevTools, direct localStorage manipulation). Set to null to disable polling.
89
+ * defaults to 2000ms.
90
+ */
91
+ pollingIntervalMs?: number | null;
92
+ }
93
+ declare function useStoragePersistedState<T>(key: string, defaultValue: Exclude<T, null | undefined>, options?: StoragePersistedStateOptions<T>): [T, (newValue: T | ((prev: T) => T)) => void, () => void];
94
+ declare function useStoragePersistedState<T>(key: string, defaultValue: null | undefined, options: StoragePersistedStateOptions<T> & {
95
+ codec: Codec<T>;
96
+ }): [T | null, (newValue: T | ((prev: T) => T)) => void, () => void];
97
+
98
+ /**
99
+ * Read a persisted value from storage using the same codec behavior as the hook.
100
+ *
101
+ * Returns the provided defaultValue when the key is missing or parsing fails.
102
+ */
103
+ declare function readStoragePersistedState<T>(key: string, defaultValue: Exclude<T, null | undefined>, options?: StoragePersistedStateOptions<T>): T;
104
+ /**
105
+ * Read a persisted value from storage using an explicit codec.
106
+ *
107
+ * Use this overload when defaultValue is null or undefined.
108
+ */
109
+ declare function readStoragePersistedState<T>(key: string, defaultValue: null | undefined, options: StoragePersistedStateOptions<T> & {
110
+ codec: Codec<T>;
111
+ }): T | null;
112
+ /**
113
+ * Set a persisted value in storage and notify active hooks for the same key.
114
+ *
115
+ * Supports functional updates using the current decoded value.
116
+ */
117
+ declare function setStoragePersistedState<T>(key: string, newValue: Exclude<T, null | undefined>, options?: StoragePersistedStateOptions<T>): void;
118
+ /**
119
+ * Set a persisted value in storage using an explicit codec and notify listeners.
120
+ *
121
+ * Use this overload when the new value is null or undefined or when you want custom serialization.
122
+ */
123
+ declare function setStoragePersistedState<T>(key: string, newValue: T | ((prev: T | null) => T), options: StoragePersistedStateOptions<T> & {
124
+ codec: Codec<T>;
125
+ }): void;
126
+
127
+ export { BooleanCodec, type Codec, JsonCodec, NumberCodec, type StoragePersistedStateOptions, type StorageType, StringCodec, inferCodec, readStoragePersistedState, setStoragePersistedState, useStoragePersistedState };
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Defines how to serialize/deserialize a value from localStorage.
3
+ * null means the key doesn't exist.
4
+ */
5
+ interface Codec<T> {
6
+ encode: (value: T) => string | null;
7
+ decode: (value: string | null) => T;
8
+ }
9
+ /**
10
+ * A robust JSON codec that handles parsing errors gracefully.
11
+ * Works with objects, arrays, and other JSON-serializable values.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const [user, setUser] = useStoragePersistedState<User | null>(
16
+ * "user",
17
+ * null,
18
+ * { codec: JsonCodec }
19
+ * );
20
+ * ```
21
+ */
22
+ declare const JsonCodec: Codec<unknown>;
23
+ /**
24
+ * Codec for string values. Stores strings as-is without any transformation.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const [name, setName] = useStoragePersistedState("name", "Guest");
29
+ * // Automatically uses StringCodec because defaultValue is a string
30
+ * ```
31
+ */
32
+ declare const StringCodec: Codec<string | null>;
33
+ /**
34
+ * Codec for boolean values. Stores as "true" or "false" strings.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const [isDark, setIsDark] = useStoragePersistedState("darkMode", false);
39
+ * // Automatically uses BooleanCodec because defaultValue is a boolean
40
+ * ```
41
+ */
42
+ declare const BooleanCodec: Codec<boolean>;
43
+ /**
44
+ * Codec for number values. Handles NaN, Infinity, and -Infinity correctly.
45
+ * Returns null for unparseable values (e.g., empty string, invalid number string).
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * const [count, setCount] = useStoragePersistedState("count", 0);
50
+ * // Automatically uses NumberCodec because defaultValue is a number
51
+ * ```
52
+ */
53
+ declare const NumberCodec: Codec<number | null>;
54
+ /**
55
+ * Infers the appropriate codec based on the default value's type.
56
+ * Used internally when the user doesn't provide an explicit codec.
57
+ *
58
+ * - `boolean` → BooleanCodec
59
+ * - `number` → NumberCodec
60
+ * - `string` → StringCodec
61
+ * - `object`, `array`, `undefined`, `null` → JsonCodec
62
+ *
63
+ * @param defaultValue - The default value to infer the codec from
64
+ * @returns The inferred codec for the given value type
65
+ */
66
+ declare function inferCodec<T>(defaultValue: T): Codec<T>;
67
+
68
+ type StorageType = "localStorage" | "sessionStorage" | "memory";
69
+ /**
70
+ * Options for the useStoragePersistedState hook
71
+ */
72
+ interface StoragePersistedStateOptions<T> {
73
+ /**
74
+ * Explicit codec for when defaultValue is null or undefined, for complex types, or special cases (e.g., data migration on read).
75
+ * If not provided, codec is inferred from defaultValue type.
76
+ */
77
+ codec?: Codec<T>;
78
+ /**
79
+ * Storage type to use: 'localStorage' (default), 'sessionStorage', or 'memory'.
80
+ */
81
+ storageType?: StorageType;
82
+ /**
83
+ * Enable cross-tab synchronization via the StorageEvent. Note: disabling this will not stop other tabs from updating localStorage, but this hook will not automatically respond to those changes.
84
+ * defaults to true.
85
+ */
86
+ crossTabSync?: boolean;
87
+ /**
88
+ * Polling interval in milliseconds for detecting storage changes made outside of React (e.g., DevTools, direct localStorage manipulation). Set to null to disable polling.
89
+ * defaults to 2000ms.
90
+ */
91
+ pollingIntervalMs?: number | null;
92
+ }
93
+ declare function useStoragePersistedState<T>(key: string, defaultValue: Exclude<T, null | undefined>, options?: StoragePersistedStateOptions<T>): [T, (newValue: T | ((prev: T) => T)) => void, () => void];
94
+ declare function useStoragePersistedState<T>(key: string, defaultValue: null | undefined, options: StoragePersistedStateOptions<T> & {
95
+ codec: Codec<T>;
96
+ }): [T | null, (newValue: T | ((prev: T) => T)) => void, () => void];
97
+
98
+ /**
99
+ * Read a persisted value from storage using the same codec behavior as the hook.
100
+ *
101
+ * Returns the provided defaultValue when the key is missing or parsing fails.
102
+ */
103
+ declare function readStoragePersistedState<T>(key: string, defaultValue: Exclude<T, null | undefined>, options?: StoragePersistedStateOptions<T>): T;
104
+ /**
105
+ * Read a persisted value from storage using an explicit codec.
106
+ *
107
+ * Use this overload when defaultValue is null or undefined.
108
+ */
109
+ declare function readStoragePersistedState<T>(key: string, defaultValue: null | undefined, options: StoragePersistedStateOptions<T> & {
110
+ codec: Codec<T>;
111
+ }): T | null;
112
+ /**
113
+ * Set a persisted value in storage and notify active hooks for the same key.
114
+ *
115
+ * Supports functional updates using the current decoded value.
116
+ */
117
+ declare function setStoragePersistedState<T>(key: string, newValue: Exclude<T, null | undefined>, options?: StoragePersistedStateOptions<T>): void;
118
+ /**
119
+ * Set a persisted value in storage using an explicit codec and notify listeners.
120
+ *
121
+ * Use this overload when the new value is null or undefined or when you want custom serialization.
122
+ */
123
+ declare function setStoragePersistedState<T>(key: string, newValue: T | ((prev: T | null) => T), options: StoragePersistedStateOptions<T> & {
124
+ codec: Codec<T>;
125
+ }): void;
126
+
127
+ export { BooleanCodec, type Codec, JsonCodec, NumberCodec, type StoragePersistedStateOptions, type StorageType, StringCodec, inferCodec, readStoragePersistedState, setStoragePersistedState, useStoragePersistedState };