@usefy/use-session-storage 0.0.20 → 0.0.22
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/README.md +51 -8
- package/dist/index.d.mts +20 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +83 -34
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +84 -35
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<h1 align="center">@usefy/use-session-storage</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>A lightweight React hook for persisting state in sessionStorage</strong>
|
|
8
|
+
<strong>A lightweight React hook for persisting state in sessionStorage with automatic same-tab synchronization</strong>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
## Overview
|
|
37
37
|
|
|
38
|
-
`@usefy/use-session-storage` provides a `useState`-like API for persisting data in sessionStorage. Data persists during the browser session (tab lifetime) but clears when the tab is closed. Each tab has isolated storage, making it perfect for temporary form data, wizard steps, and session-specific state.
|
|
38
|
+
`@usefy/use-session-storage` provides a `useState`-like API for persisting data in sessionStorage. Features include **same-tab component synchronization**, custom serialization, lazy initialization, and error handling. Data persists during the browser session (tab lifetime) but clears when the tab is closed. Each tab has isolated storage, making it perfect for temporary form data, wizard steps, and session-specific state.
|
|
39
39
|
|
|
40
40
|
**Part of the [@usefy](https://www.npmjs.com/org/usefy) ecosystem** — a collection of production-ready React hooks designed for modern applications.
|
|
41
41
|
|
|
@@ -44,7 +44,9 @@
|
|
|
44
44
|
- **Zero Dependencies** — Pure React implementation with no external dependencies
|
|
45
45
|
- **TypeScript First** — Full type safety with generics and exported interfaces
|
|
46
46
|
- **useState-like API** — Familiar tuple return: `[value, setValue, removeValue]`
|
|
47
|
+
- **Same-Tab Sync** — Multiple components using the same key stay in sync automatically
|
|
47
48
|
- **Tab Isolation** — Each browser tab has its own session storage
|
|
49
|
+
- **React 18+ Optimized** — Built with `useSyncExternalStore` for Concurrent Mode compatibility
|
|
48
50
|
- **Auto-Cleanup** — Data cleared automatically when tab closes
|
|
49
51
|
- **Custom Serialization** — Support for Date, Map, Set, or any custom type
|
|
50
52
|
- **Lazy Initialization** — Function initializer support for expensive defaults
|
|
@@ -165,6 +167,39 @@ A hook that persists state in sessionStorage for the duration of the browser ses
|
|
|
165
167
|
|
|
166
168
|
## Examples
|
|
167
169
|
|
|
170
|
+
### Same-Tab Component Synchronization
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
import { useSessionStorage } from "@usefy/use-session-storage";
|
|
174
|
+
|
|
175
|
+
// Multiple components using the same key automatically stay in sync!
|
|
176
|
+
function WizardProgress() {
|
|
177
|
+
const [step] = useSessionStorage("wizard-step", 1);
|
|
178
|
+
return <ProgressBar current={step} total={5} />;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function WizardNavigation() {
|
|
182
|
+
const [step, setStep] = useSessionStorage("wizard-step", 1);
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<div>
|
|
186
|
+
<button onClick={() => setStep((s) => s - 1)} disabled={step === 1}>
|
|
187
|
+
Back
|
|
188
|
+
</button>
|
|
189
|
+
<button onClick={() => setStep((s) => s + 1)} disabled={step === 5}>
|
|
190
|
+
Next {/* WizardProgress automatically updates! */}
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function WizardContent() {
|
|
197
|
+
const [step] = useSessionStorage("wizard-step", 1);
|
|
198
|
+
// Also updates when WizardNavigation changes step!
|
|
199
|
+
return <StepContent step={step} />;
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
168
203
|
### Multi-Step Wizard
|
|
169
204
|
|
|
170
205
|
```tsx
|
|
@@ -487,12 +522,7 @@ This package maintains comprehensive test coverage to ensure reliability and sta
|
|
|
487
522
|
|
|
488
523
|
### Test Coverage
|
|
489
524
|
|
|
490
|
-
|
|
491
|
-
| ---------- | -------------- |
|
|
492
|
-
| Statements | 93.75% (45/48) |
|
|
493
|
-
| Branches | 78.94% (15/19) |
|
|
494
|
-
| Functions | 100% (6/6) |
|
|
495
|
-
| Lines | 93.75% (45/48) |
|
|
525
|
+
📊 <a href="https://geon0529.github.io/usefy/coverage/use-session-storage/src/index.html" target="_blank" rel="noopener noreferrer"><strong>View Detailed Coverage Report</strong></a> (GitHub Pages)
|
|
496
526
|
|
|
497
527
|
### Test Categories
|
|
498
528
|
|
|
@@ -508,6 +538,19 @@ This package maintains comprehensive test coverage to ensure reliability and sta
|
|
|
508
538
|
|
|
509
539
|
</details>
|
|
510
540
|
|
|
541
|
+
<details>
|
|
542
|
+
<summary><strong>Same-Tab Sync Tests</strong></summary>
|
|
543
|
+
|
|
544
|
+
- Sync ComponentB when ComponentA updates the same key
|
|
545
|
+
- Sync multiple components using the same key
|
|
546
|
+
- Sync when using functional updates
|
|
547
|
+
- Sync when removeValue is called
|
|
548
|
+
- Not affect components with different keys
|
|
549
|
+
- Handle rapid updates from different components
|
|
550
|
+
- Cleanup listeners on unmount
|
|
551
|
+
|
|
552
|
+
</details>
|
|
553
|
+
|
|
511
554
|
<details>
|
|
512
555
|
<summary><strong>setValue Tests</strong></summary>
|
|
513
556
|
|
package/dist/index.d.mts
CHANGED
|
@@ -33,9 +33,13 @@ type UseSessionStorageReturn<T> = readonly [
|
|
|
33
33
|
() => void
|
|
34
34
|
];
|
|
35
35
|
/**
|
|
36
|
-
* A hook for persisting state in sessionStorage.
|
|
36
|
+
* A hook for persisting state in sessionStorage with automatic synchronization.
|
|
37
37
|
* Works like useState but persists the value in sessionStorage for the duration of the browser session.
|
|
38
38
|
*
|
|
39
|
+
* Features:
|
|
40
|
+
* - Same-tab synchronization: Multiple components using the same key will stay in sync
|
|
41
|
+
* - SSR compatible: Works with Next.js, Remix, and other SSR frameworks
|
|
42
|
+
*
|
|
39
43
|
* Unlike localStorage, sessionStorage data:
|
|
40
44
|
* - Is cleared when the tab/window is closed
|
|
41
45
|
* - Is not shared between tabs (each tab has its own session)
|
|
@@ -69,6 +73,21 @@ type UseSessionStorageReturn<T> = readonly [
|
|
|
69
73
|
*
|
|
70
74
|
* @example
|
|
71
75
|
* ```tsx
|
|
76
|
+
* // Same-tab synchronization - both components stay in sync
|
|
77
|
+
* function ComponentA() {
|
|
78
|
+
* const [step, setStep] = useSessionStorage('wizard-step', 1);
|
|
79
|
+
* return <button onClick={() => setStep(s => s + 1)}>Next Step</button>;
|
|
80
|
+
* }
|
|
81
|
+
*
|
|
82
|
+
* function ComponentB() {
|
|
83
|
+
* const [step] = useSessionStorage('wizard-step', 1);
|
|
84
|
+
* // Automatically updates when ComponentA calls setStep!
|
|
85
|
+
* return <p>Current Step: {step}</p>;
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```tsx
|
|
72
91
|
* // Temporary state that resets on tab close
|
|
73
92
|
* const [wizardStep, setWizardStep] = useSessionStorage('wizard-step', 1);
|
|
74
93
|
* ```
|
package/dist/index.d.ts
CHANGED
|
@@ -33,9 +33,13 @@ type UseSessionStorageReturn<T> = readonly [
|
|
|
33
33
|
() => void
|
|
34
34
|
];
|
|
35
35
|
/**
|
|
36
|
-
* A hook for persisting state in sessionStorage.
|
|
36
|
+
* A hook for persisting state in sessionStorage with automatic synchronization.
|
|
37
37
|
* Works like useState but persists the value in sessionStorage for the duration of the browser session.
|
|
38
38
|
*
|
|
39
|
+
* Features:
|
|
40
|
+
* - Same-tab synchronization: Multiple components using the same key will stay in sync
|
|
41
|
+
* - SSR compatible: Works with Next.js, Remix, and other SSR frameworks
|
|
42
|
+
*
|
|
39
43
|
* Unlike localStorage, sessionStorage data:
|
|
40
44
|
* - Is cleared when the tab/window is closed
|
|
41
45
|
* - Is not shared between tabs (each tab has its own session)
|
|
@@ -69,6 +73,21 @@ type UseSessionStorageReturn<T> = readonly [
|
|
|
69
73
|
*
|
|
70
74
|
* @example
|
|
71
75
|
* ```tsx
|
|
76
|
+
* // Same-tab synchronization - both components stay in sync
|
|
77
|
+
* function ComponentA() {
|
|
78
|
+
* const [step, setStep] = useSessionStorage('wizard-step', 1);
|
|
79
|
+
* return <button onClick={() => setStep(s => s + 1)}>Next Step</button>;
|
|
80
|
+
* }
|
|
81
|
+
*
|
|
82
|
+
* function ComponentB() {
|
|
83
|
+
* const [step] = useSessionStorage('wizard-step', 1);
|
|
84
|
+
* // Automatically updates when ComponentA calls setStep!
|
|
85
|
+
* return <p>Current Step: {step}</p>;
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```tsx
|
|
72
91
|
* // Temporary state that resets on tab close
|
|
73
92
|
* const [wizardStep, setWizardStep] = useSessionStorage('wizard-step', 1);
|
|
74
93
|
* ```
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,30 @@ module.exports = __toCommonJS(index_exports);
|
|
|
26
26
|
|
|
27
27
|
// src/useSessionStorage.ts
|
|
28
28
|
var import_react = require("react");
|
|
29
|
+
|
|
30
|
+
// src/store.ts
|
|
31
|
+
var listeners = /* @__PURE__ */ new Map();
|
|
32
|
+
function subscribe(key, listener) {
|
|
33
|
+
if (!listeners.has(key)) {
|
|
34
|
+
listeners.set(key, /* @__PURE__ */ new Set());
|
|
35
|
+
}
|
|
36
|
+
const keyListeners = listeners.get(key);
|
|
37
|
+
keyListeners.add(listener);
|
|
38
|
+
return () => {
|
|
39
|
+
keyListeners.delete(listener);
|
|
40
|
+
if (keyListeners.size === 0) {
|
|
41
|
+
listeners.delete(key);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function notifyListeners(key) {
|
|
46
|
+
const keyListeners = listeners.get(key);
|
|
47
|
+
if (keyListeners) {
|
|
48
|
+
keyListeners.forEach((listener) => listener());
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/useSessionStorage.ts
|
|
29
53
|
function resolveInitialValue(initialValue) {
|
|
30
54
|
return typeof initialValue === "function" ? initialValue() : initialValue;
|
|
31
55
|
}
|
|
@@ -38,40 +62,79 @@ function useSessionStorage(key, initialValue, options = {}) {
|
|
|
38
62
|
const serializerRef = (0, import_react.useRef)(serializer);
|
|
39
63
|
const deserializerRef = (0, import_react.useRef)(deserializer);
|
|
40
64
|
const onErrorRef = (0, import_react.useRef)(onError);
|
|
65
|
+
const initialValueRef = (0, import_react.useRef)(initialValue);
|
|
41
66
|
serializerRef.current = serializer;
|
|
42
67
|
deserializerRef.current = deserializer;
|
|
43
68
|
onErrorRef.current = onError;
|
|
44
|
-
const initialValueRef = (0, import_react.useRef)(initialValue);
|
|
45
69
|
initialValueRef.current = initialValue;
|
|
70
|
+
const cacheRef = (0, import_react.useRef)(
|
|
71
|
+
null
|
|
72
|
+
);
|
|
46
73
|
const isClient = typeof window !== "undefined";
|
|
47
|
-
const
|
|
74
|
+
const subscribeToStore = (0, import_react.useCallback)(
|
|
75
|
+
(onStoreChange) => {
|
|
76
|
+
const unsubscribeStore = subscribe(key, onStoreChange);
|
|
77
|
+
return () => {
|
|
78
|
+
unsubscribeStore();
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
[key]
|
|
82
|
+
);
|
|
83
|
+
const getSnapshot = (0, import_react.useCallback)(() => {
|
|
48
84
|
if (!isClient) {
|
|
49
|
-
return resolveInitialValue(
|
|
85
|
+
return resolveInitialValue(initialValueRef.current);
|
|
50
86
|
}
|
|
51
87
|
try {
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
54
|
-
return
|
|
88
|
+
const rawValue = window.sessionStorage.getItem(key);
|
|
89
|
+
if (cacheRef.current && cacheRef.current.rawValue === rawValue) {
|
|
90
|
+
return cacheRef.current.parsedValue;
|
|
91
|
+
}
|
|
92
|
+
let parsedValue;
|
|
93
|
+
if (rawValue !== null) {
|
|
94
|
+
parsedValue = deserializerRef.current(rawValue);
|
|
95
|
+
} else {
|
|
96
|
+
parsedValue = resolveInitialValue(initialValueRef.current);
|
|
55
97
|
}
|
|
56
|
-
|
|
98
|
+
cacheRef.current = { rawValue, parsedValue };
|
|
99
|
+
return parsedValue;
|
|
57
100
|
} catch (error) {
|
|
58
101
|
onErrorRef.current?.(error);
|
|
59
|
-
|
|
102
|
+
const fallbackValue = resolveInitialValue(initialValueRef.current);
|
|
103
|
+
cacheRef.current = { rawValue: null, parsedValue: fallbackValue };
|
|
104
|
+
return fallbackValue;
|
|
60
105
|
}
|
|
61
|
-
});
|
|
62
|
-
const
|
|
63
|
-
|
|
106
|
+
}, [key, isClient]);
|
|
107
|
+
const getServerSnapshot = (0, import_react.useCallback)(() => {
|
|
108
|
+
return resolveInitialValue(initialValueRef.current);
|
|
109
|
+
}, []);
|
|
110
|
+
const storedValue = (0, import_react.useSyncExternalStore)(
|
|
111
|
+
subscribeToStore,
|
|
112
|
+
getSnapshot,
|
|
113
|
+
getServerSnapshot
|
|
114
|
+
);
|
|
64
115
|
const setValue = (0, import_react.useCallback)(
|
|
65
116
|
(value) => {
|
|
66
117
|
try {
|
|
67
|
-
const currentValue =
|
|
118
|
+
const currentValue = (() => {
|
|
119
|
+
try {
|
|
120
|
+
const item = window.sessionStorage.getItem(key);
|
|
121
|
+
if (item !== null) {
|
|
122
|
+
return deserializerRef.current(item);
|
|
123
|
+
}
|
|
124
|
+
return resolveInitialValue(initialValueRef.current);
|
|
125
|
+
} catch {
|
|
126
|
+
return resolveInitialValue(initialValueRef.current);
|
|
127
|
+
}
|
|
128
|
+
})();
|
|
68
129
|
const valueToStore = value instanceof Function ? value(currentValue) : value;
|
|
69
|
-
setStoredValue(valueToStore);
|
|
70
130
|
if (typeof window !== "undefined") {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
131
|
+
const serialized = serializerRef.current(valueToStore);
|
|
132
|
+
window.sessionStorage.setItem(key, serialized);
|
|
133
|
+
cacheRef.current = {
|
|
134
|
+
rawValue: serialized,
|
|
135
|
+
parsedValue: valueToStore
|
|
136
|
+
};
|
|
137
|
+
notifyListeners(key);
|
|
75
138
|
}
|
|
76
139
|
} catch (error) {
|
|
77
140
|
onErrorRef.current?.(error);
|
|
@@ -81,30 +144,16 @@ function useSessionStorage(key, initialValue, options = {}) {
|
|
|
81
144
|
);
|
|
82
145
|
const removeValue = (0, import_react.useCallback)(() => {
|
|
83
146
|
try {
|
|
84
|
-
const initial = resolveInitialValue(initialValueRef.current);
|
|
85
|
-
setStoredValue(initial);
|
|
86
147
|
if (typeof window !== "undefined") {
|
|
87
148
|
window.sessionStorage.removeItem(key);
|
|
149
|
+
const initialVal = resolveInitialValue(initialValueRef.current);
|
|
150
|
+
cacheRef.current = { rawValue: null, parsedValue: initialVal };
|
|
151
|
+
notifyListeners(key);
|
|
88
152
|
}
|
|
89
153
|
} catch (error) {
|
|
90
154
|
onErrorRef.current?.(error);
|
|
91
155
|
}
|
|
92
156
|
}, [key]);
|
|
93
|
-
(0, import_react.useEffect)(() => {
|
|
94
|
-
if (!isClient) {
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
try {
|
|
98
|
-
const item = window.sessionStorage.getItem(key);
|
|
99
|
-
if (item !== null) {
|
|
100
|
-
setStoredValue(deserializerRef.current(item));
|
|
101
|
-
} else {
|
|
102
|
-
setStoredValue(resolveInitialValue(initialValueRef.current));
|
|
103
|
-
}
|
|
104
|
-
} catch {
|
|
105
|
-
setStoredValue(resolveInitialValue(initialValueRef.current));
|
|
106
|
-
}
|
|
107
|
-
}, [key]);
|
|
108
157
|
return [storedValue, setValue, removeValue];
|
|
109
158
|
}
|
|
110
159
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/useSessionStorage.ts"],"sourcesContent":["export {\n useSessionStorage,\n type UseSessionStorageOptions,\n type UseSessionStorageReturn,\n type InitialValue,\n} from \"./useSessionStorage\";\n","import { useCallback, useEffect, useRef, useState } from \"react\";\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 useSessionStorage hook\n */\nexport interface UseSessionStorageOptions<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 * Callback function called when an error occurs\n */\n onError?: (error: Error) => void;\n}\n\n/**\n * Return type for useSessionStorage hook - tuple similar to useState\n */\nexport type UseSessionStorageReturn<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 sessionStorage */\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 sessionStorage.\n * Works like useState but persists the value in sessionStorage for the duration of the browser session.\n *\n * Unlike localStorage, sessionStorage data:\n * - Is cleared when the tab/window is closed\n * - Is not shared between tabs (each tab has its own session)\n *\n * @template T - The type of the stored value\n * @param key - The sessionStorage 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 and error handling\n * @returns A tuple of [storedValue, setValue, removeValue]\n *\n * @example\n * ```tsx\n * // Basic usage - form data that persists during session\n * function CheckoutForm() {\n * const [formData, setFormData, clearForm] = useSessionStorage('checkout-form', {\n * name: '',\n * email: '',\n * });\n *\n * return (\n * <form>\n * <input\n * value={formData.name}\n * onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}\n * />\n * <button type=\"button\" onClick={clearForm}>Clear</button>\n * </form>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Temporary state that resets on tab close\n * const [wizardStep, setWizardStep] = useSessionStorage('wizard-step', 1);\n * ```\n *\n * @example\n * ```tsx\n * // With lazy initialization\n * const [cache, setCache] = useSessionStorage('cache', () => computeInitialCache());\n * ```\n *\n * @example\n * ```tsx\n * // With custom serializer/deserializer\n * const [date, setDate] = useSessionStorage<Date>('lastAction', new Date(), {\n * serializer: (d) => d.toISOString(),\n * deserializer: (s) => new Date(s),\n * });\n * ```\n */\nexport function useSessionStorage<T>(\n key: string,\n initialValue: InitialValue<T>,\n options: UseSessionStorageOptions<T> = {}\n): UseSessionStorageReturn<T> {\n const {\n serializer = JSON.stringify,\n deserializer = JSON.parse,\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 serializerRef.current = serializer;\n deserializerRef.current = deserializer;\n onErrorRef.current = onError;\n\n // Store initialValue in ref for use in removeValue\n const initialValueRef = useRef(initialValue);\n initialValueRef.current = initialValue;\n\n // SSR check\n const isClient = typeof window !== \"undefined\";\n\n // Lazy initialization with sessionStorage read\n const [storedValue, setStoredValue] = useState<T>(() => {\n if (!isClient) {\n return resolveInitialValue(initialValue);\n }\n\n try {\n const item = window.sessionStorage.getItem(key);\n if (item !== null) {\n return deserializerRef.current(item);\n }\n return resolveInitialValue(initialValue);\n } catch (error) {\n onErrorRef.current?.(error as Error);\n return resolveInitialValue(initialValue);\n }\n });\n\n // Store current value in ref for stable setValue reference\n const storedValueRef = useRef<T>(storedValue);\n storedValueRef.current = storedValue;\n\n // setValue - stable reference (only depends on key)\n const setValue = useCallback<React.Dispatch<React.SetStateAction<T>>>(\n (value) => {\n try {\n const currentValue = storedValueRef.current;\n const valueToStore =\n value instanceof Function ? value(currentValue) : value;\n\n setStoredValue(valueToStore);\n\n if (typeof window !== \"undefined\") {\n window.sessionStorage.setItem(\n key,\n serializerRef.current(valueToStore)\n );\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 const initial = resolveInitialValue(initialValueRef.current);\n setStoredValue(initial);\n\n if (typeof window !== \"undefined\") {\n window.sessionStorage.removeItem(key);\n }\n } catch (error) {\n onErrorRef.current?.(error as Error);\n }\n }, [key]);\n\n // Re-read value when key changes\n useEffect(() => {\n if (!isClient) {\n return;\n }\n\n try {\n const item = window.sessionStorage.getItem(key);\n if (item !== null) {\n setStoredValue(deserializerRef.current(item));\n } else {\n setStoredValue(resolveInitialValue(initialValueRef.current));\n }\n } catch {\n setStoredValue(resolveInitialValue(initialValueRef.current));\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [key]);\n\n return [storedValue, setValue, removeValue] as const;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAAyD;AA0CzD,SAAS,oBAAuB,cAAkC;AAChE,SAAO,OAAO,iBAAiB,aAC1B,aAAyB,IAC1B;AACN;AA0DO,SAAS,kBACd,KACA,cACA,UAAuC,CAAC,GACZ;AAC5B,QAAM;AAAA,IACJ,aAAa,KAAK;AAAA,IAClB,eAAe,KAAK;AAAA,IACpB;AAAA,EACF,IAAI;AAGJ,QAAM,oBAAgB,qBAAO,UAAU;AACvC,QAAM,sBAAkB,qBAAO,YAAY;AAC3C,QAAM,iBAAa,qBAAO,OAAO;AACjC,gBAAc,UAAU;AACxB,kBAAgB,UAAU;AAC1B,aAAW,UAAU;AAGrB,QAAM,sBAAkB,qBAAO,YAAY;AAC3C,kBAAgB,UAAU;AAG1B,QAAM,WAAW,OAAO,WAAW;AAGnC,QAAM,CAAC,aAAa,cAAc,QAAI,uBAAY,MAAM;AACtD,QAAI,CAAC,UAAU;AACb,aAAO,oBAAoB,YAAY;AAAA,IACzC;AAEA,QAAI;AACF,YAAM,OAAO,OAAO,eAAe,QAAQ,GAAG;AAC9C,UAAI,SAAS,MAAM;AACjB,eAAO,gBAAgB,QAAQ,IAAI;AAAA,MACrC;AACA,aAAO,oBAAoB,YAAY;AAAA,IACzC,SAAS,OAAO;AACd,iBAAW,UAAU,KAAc;AACnC,aAAO,oBAAoB,YAAY;AAAA,IACzC;AAAA,EACF,CAAC;AAGD,QAAM,qBAAiB,qBAAU,WAAW;AAC5C,iBAAe,UAAU;AAGzB,QAAM,eAAW;AAAA,IACf,CAAC,UAAU;AACT,UAAI;AACF,cAAM,eAAe,eAAe;AACpC,cAAM,eACJ,iBAAiB,WAAW,MAAM,YAAY,IAAI;AAEpD,uBAAe,YAAY;AAE3B,YAAI,OAAO,WAAW,aAAa;AACjC,iBAAO,eAAe;AAAA,YACpB;AAAA,YACA,cAAc,QAAQ,YAAY;AAAA,UACpC;AAAA,QACF;AAAA,MACF,SAAS,OAAO;AACd,mBAAW,UAAU,KAAc;AAAA,MACrC;AAAA,IACF;AAAA,IACA,CAAC,GAAG;AAAA,EACN;AAGA,QAAM,kBAAc,0BAAY,MAAM;AACpC,QAAI;AACF,YAAM,UAAU,oBAAoB,gBAAgB,OAAO;AAC3D,qBAAe,OAAO;AAEtB,UAAI,OAAO,WAAW,aAAa;AACjC,eAAO,eAAe,WAAW,GAAG;AAAA,MACtC;AAAA,IACF,SAAS,OAAO;AACd,iBAAW,UAAU,KAAc;AAAA,IACrC;AAAA,EACF,GAAG,CAAC,GAAG,CAAC;AAGR,8BAAU,MAAM;AACd,QAAI,CAAC,UAAU;AACb;AAAA,IACF;AAEA,QAAI;AACF,YAAM,OAAO,OAAO,eAAe,QAAQ,GAAG;AAC9C,UAAI,SAAS,MAAM;AACjB,uBAAe,gBAAgB,QAAQ,IAAI,CAAC;AAAA,MAC9C,OAAO;AACL,uBAAe,oBAAoB,gBAAgB,OAAO,CAAC;AAAA,MAC7D;AAAA,IACF,QAAQ;AACN,qBAAe,oBAAoB,gBAAgB,OAAO,CAAC;AAAA,IAC7D;AAAA,EAEF,GAAG,CAAC,GAAG,CAAC;AAER,SAAO,CAAC,aAAa,UAAU,WAAW;AAC5C;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/useSessionStorage.ts","../src/store.ts"],"sourcesContent":["export {\n useSessionStorage,\n type UseSessionStorageOptions,\n type UseSessionStorageReturn,\n type InitialValue,\n} from \"./useSessionStorage\";\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 useSessionStorage hook\n */\nexport interface UseSessionStorageOptions<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 * Callback function called when an error occurs\n */\n onError?: (error: Error) => void;\n}\n\n/**\n * Return type for useSessionStorage hook - tuple similar to useState\n */\nexport type UseSessionStorageReturn<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 sessionStorage */\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 sessionStorage with automatic synchronization.\n * Works like useState but persists the value in sessionStorage for the duration of the browser session.\n *\n * Features:\n * - Same-tab synchronization: Multiple components using the same key will stay in sync\n * - SSR compatible: Works with Next.js, Remix, and other SSR frameworks\n *\n * Unlike localStorage, sessionStorage data:\n * - Is cleared when the tab/window is closed\n * - Is not shared between tabs (each tab has its own session)\n *\n * @template T - The type of the stored value\n * @param key - The sessionStorage 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 and error handling\n * @returns A tuple of [storedValue, setValue, removeValue]\n *\n * @example\n * ```tsx\n * // Basic usage - form data that persists during session\n * function CheckoutForm() {\n * const [formData, setFormData, clearForm] = useSessionStorage('checkout-form', {\n * name: '',\n * email: '',\n * });\n *\n * return (\n * <form>\n * <input\n * value={formData.name}\n * onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}\n * />\n * <button type=\"button\" onClick={clearForm}>Clear</button>\n * </form>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Same-tab synchronization - both components stay in sync\n * function ComponentA() {\n * const [step, setStep] = useSessionStorage('wizard-step', 1);\n * return <button onClick={() => setStep(s => s + 1)}>Next Step</button>;\n * }\n *\n * function ComponentB() {\n * const [step] = useSessionStorage('wizard-step', 1);\n * // Automatically updates when ComponentA calls setStep!\n * return <p>Current Step: {step}</p>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Temporary state that resets on tab close\n * const [wizardStep, setWizardStep] = useSessionStorage('wizard-step', 1);\n * ```\n *\n * @example\n * ```tsx\n * // With lazy initialization\n * const [cache, setCache] = useSessionStorage('cache', () => computeInitialCache());\n * ```\n *\n * @example\n * ```tsx\n * // With custom serializer/deserializer\n * const [date, setDate] = useSessionStorage<Date>('lastAction', new Date(), {\n * serializer: (d) => d.toISOString(),\n * deserializer: (s) => new Date(s),\n * });\n * ```\n */\nexport function useSessionStorage<T>(\n key: string,\n initialValue: InitialValue<T>,\n options: UseSessionStorageOptions<T> = {}\n): UseSessionStorageReturn<T> {\n const {\n serializer = JSON.stringify,\n deserializer = JSON.parse,\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 same-tab synchronization (sessionStorage doesn't have cross-tab sync)\n const subscribeToStore = useCallback(\n (onStoreChange: () => void) => {\n // Subscribe to same-tab changes via internal store\n const unsubscribeStore = subscribe(key, onStoreChange);\n\n // Note: sessionStorage doesn't fire storage events for changes in the same tab,\n // and changes in other tabs don't affect this tab's sessionStorage.\n // So we only use the internal store for synchronization.\n\n return () => {\n unsubscribeStore();\n };\n },\n [key]\n );\n\n // getSnapshot: Read current value from sessionStorage with caching\n const getSnapshot = useCallback((): T => {\n if (!isClient) {\n return resolveInitialValue(initialValueRef.current);\n }\n\n try {\n const rawValue = window.sessionStorage.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 sessionStorage 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.sessionStorage.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.sessionStorage.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.sessionStorage.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 sessionStorage synchronization\n * This module manages listeners for same-tab synchronization across components\n * using the same sessionStorage 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 sessionStorage 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 sessionStorage 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,29 @@
|
|
|
1
1
|
// src/useSessionStorage.ts
|
|
2
|
-
import { useCallback,
|
|
2
|
+
import { useCallback, useRef, useSyncExternalStore } from "react";
|
|
3
|
+
|
|
4
|
+
// src/store.ts
|
|
5
|
+
var listeners = /* @__PURE__ */ new Map();
|
|
6
|
+
function subscribe(key, listener) {
|
|
7
|
+
if (!listeners.has(key)) {
|
|
8
|
+
listeners.set(key, /* @__PURE__ */ new Set());
|
|
9
|
+
}
|
|
10
|
+
const keyListeners = listeners.get(key);
|
|
11
|
+
keyListeners.add(listener);
|
|
12
|
+
return () => {
|
|
13
|
+
keyListeners.delete(listener);
|
|
14
|
+
if (keyListeners.size === 0) {
|
|
15
|
+
listeners.delete(key);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function notifyListeners(key) {
|
|
20
|
+
const keyListeners = listeners.get(key);
|
|
21
|
+
if (keyListeners) {
|
|
22
|
+
keyListeners.forEach((listener) => listener());
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/useSessionStorage.ts
|
|
3
27
|
function resolveInitialValue(initialValue) {
|
|
4
28
|
return typeof initialValue === "function" ? initialValue() : initialValue;
|
|
5
29
|
}
|
|
@@ -12,40 +36,79 @@ function useSessionStorage(key, initialValue, options = {}) {
|
|
|
12
36
|
const serializerRef = useRef(serializer);
|
|
13
37
|
const deserializerRef = useRef(deserializer);
|
|
14
38
|
const onErrorRef = useRef(onError);
|
|
39
|
+
const initialValueRef = useRef(initialValue);
|
|
15
40
|
serializerRef.current = serializer;
|
|
16
41
|
deserializerRef.current = deserializer;
|
|
17
42
|
onErrorRef.current = onError;
|
|
18
|
-
const initialValueRef = useRef(initialValue);
|
|
19
43
|
initialValueRef.current = initialValue;
|
|
44
|
+
const cacheRef = useRef(
|
|
45
|
+
null
|
|
46
|
+
);
|
|
20
47
|
const isClient = typeof window !== "undefined";
|
|
21
|
-
const
|
|
48
|
+
const subscribeToStore = useCallback(
|
|
49
|
+
(onStoreChange) => {
|
|
50
|
+
const unsubscribeStore = subscribe(key, onStoreChange);
|
|
51
|
+
return () => {
|
|
52
|
+
unsubscribeStore();
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
[key]
|
|
56
|
+
);
|
|
57
|
+
const getSnapshot = useCallback(() => {
|
|
22
58
|
if (!isClient) {
|
|
23
|
-
return resolveInitialValue(
|
|
59
|
+
return resolveInitialValue(initialValueRef.current);
|
|
24
60
|
}
|
|
25
61
|
try {
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
return
|
|
62
|
+
const rawValue = window.sessionStorage.getItem(key);
|
|
63
|
+
if (cacheRef.current && cacheRef.current.rawValue === rawValue) {
|
|
64
|
+
return cacheRef.current.parsedValue;
|
|
29
65
|
}
|
|
30
|
-
|
|
66
|
+
let parsedValue;
|
|
67
|
+
if (rawValue !== null) {
|
|
68
|
+
parsedValue = deserializerRef.current(rawValue);
|
|
69
|
+
} else {
|
|
70
|
+
parsedValue = resolveInitialValue(initialValueRef.current);
|
|
71
|
+
}
|
|
72
|
+
cacheRef.current = { rawValue, parsedValue };
|
|
73
|
+
return parsedValue;
|
|
31
74
|
} catch (error) {
|
|
32
75
|
onErrorRef.current?.(error);
|
|
33
|
-
|
|
76
|
+
const fallbackValue = resolveInitialValue(initialValueRef.current);
|
|
77
|
+
cacheRef.current = { rawValue: null, parsedValue: fallbackValue };
|
|
78
|
+
return fallbackValue;
|
|
34
79
|
}
|
|
35
|
-
});
|
|
36
|
-
const
|
|
37
|
-
|
|
80
|
+
}, [key, isClient]);
|
|
81
|
+
const getServerSnapshot = useCallback(() => {
|
|
82
|
+
return resolveInitialValue(initialValueRef.current);
|
|
83
|
+
}, []);
|
|
84
|
+
const storedValue = useSyncExternalStore(
|
|
85
|
+
subscribeToStore,
|
|
86
|
+
getSnapshot,
|
|
87
|
+
getServerSnapshot
|
|
88
|
+
);
|
|
38
89
|
const setValue = useCallback(
|
|
39
90
|
(value) => {
|
|
40
91
|
try {
|
|
41
|
-
const currentValue =
|
|
92
|
+
const currentValue = (() => {
|
|
93
|
+
try {
|
|
94
|
+
const item = window.sessionStorage.getItem(key);
|
|
95
|
+
if (item !== null) {
|
|
96
|
+
return deserializerRef.current(item);
|
|
97
|
+
}
|
|
98
|
+
return resolveInitialValue(initialValueRef.current);
|
|
99
|
+
} catch {
|
|
100
|
+
return resolveInitialValue(initialValueRef.current);
|
|
101
|
+
}
|
|
102
|
+
})();
|
|
42
103
|
const valueToStore = value instanceof Function ? value(currentValue) : value;
|
|
43
|
-
setStoredValue(valueToStore);
|
|
44
104
|
if (typeof window !== "undefined") {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
105
|
+
const serialized = serializerRef.current(valueToStore);
|
|
106
|
+
window.sessionStorage.setItem(key, serialized);
|
|
107
|
+
cacheRef.current = {
|
|
108
|
+
rawValue: serialized,
|
|
109
|
+
parsedValue: valueToStore
|
|
110
|
+
};
|
|
111
|
+
notifyListeners(key);
|
|
49
112
|
}
|
|
50
113
|
} catch (error) {
|
|
51
114
|
onErrorRef.current?.(error);
|
|
@@ -55,30 +118,16 @@ function useSessionStorage(key, initialValue, options = {}) {
|
|
|
55
118
|
);
|
|
56
119
|
const removeValue = useCallback(() => {
|
|
57
120
|
try {
|
|
58
|
-
const initial = resolveInitialValue(initialValueRef.current);
|
|
59
|
-
setStoredValue(initial);
|
|
60
121
|
if (typeof window !== "undefined") {
|
|
61
122
|
window.sessionStorage.removeItem(key);
|
|
123
|
+
const initialVal = resolveInitialValue(initialValueRef.current);
|
|
124
|
+
cacheRef.current = { rawValue: null, parsedValue: initialVal };
|
|
125
|
+
notifyListeners(key);
|
|
62
126
|
}
|
|
63
127
|
} catch (error) {
|
|
64
128
|
onErrorRef.current?.(error);
|
|
65
129
|
}
|
|
66
130
|
}, [key]);
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
if (!isClient) {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
try {
|
|
72
|
-
const item = window.sessionStorage.getItem(key);
|
|
73
|
-
if (item !== null) {
|
|
74
|
-
setStoredValue(deserializerRef.current(item));
|
|
75
|
-
} else {
|
|
76
|
-
setStoredValue(resolveInitialValue(initialValueRef.current));
|
|
77
|
-
}
|
|
78
|
-
} catch {
|
|
79
|
-
setStoredValue(resolveInitialValue(initialValueRef.current));
|
|
80
|
-
}
|
|
81
|
-
}, [key]);
|
|
82
131
|
return [storedValue, setValue, removeValue];
|
|
83
132
|
}
|
|
84
133
|
export {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/useSessionStorage.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\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 useSessionStorage hook\n */\nexport interface UseSessionStorageOptions<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 * Callback function called when an error occurs\n */\n onError?: (error: Error) => void;\n}\n\n/**\n * Return type for useSessionStorage hook - tuple similar to useState\n */\nexport type UseSessionStorageReturn<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 sessionStorage */\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 sessionStorage.\n * Works like useState but persists the value in sessionStorage for the duration of the browser session.\n *\n * Unlike localStorage, sessionStorage data:\n * - Is cleared when the tab/window is closed\n * - Is not shared between tabs (each tab has its own session)\n *\n * @template T - The type of the stored value\n * @param key - The sessionStorage 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 and error handling\n * @returns A tuple of [storedValue, setValue, removeValue]\n *\n * @example\n * ```tsx\n * // Basic usage - form data that persists during session\n * function CheckoutForm() {\n * const [formData, setFormData, clearForm] = useSessionStorage('checkout-form', {\n * name: '',\n * email: '',\n * });\n *\n * return (\n * <form>\n * <input\n * value={formData.name}\n * onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}\n * />\n * <button type=\"button\" onClick={clearForm}>Clear</button>\n * </form>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Temporary state that resets on tab close\n * const [wizardStep, setWizardStep] = useSessionStorage('wizard-step', 1);\n * ```\n *\n * @example\n * ```tsx\n * // With lazy initialization\n * const [cache, setCache] = useSessionStorage('cache', () => computeInitialCache());\n * ```\n *\n * @example\n * ```tsx\n * // With custom serializer/deserializer\n * const [date, setDate] = useSessionStorage<Date>('lastAction', new Date(), {\n * serializer: (d) => d.toISOString(),\n * deserializer: (s) => new Date(s),\n * });\n * ```\n */\nexport function useSessionStorage<T>(\n key: string,\n initialValue: InitialValue<T>,\n options: UseSessionStorageOptions<T> = {}\n): UseSessionStorageReturn<T> {\n const {\n serializer = JSON.stringify,\n deserializer = JSON.parse,\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 serializerRef.current = serializer;\n deserializerRef.current = deserializer;\n onErrorRef.current = onError;\n\n // Store initialValue in ref for use in removeValue\n const initialValueRef = useRef(initialValue);\n initialValueRef.current = initialValue;\n\n // SSR check\n const isClient = typeof window !== \"undefined\";\n\n // Lazy initialization with sessionStorage read\n const [storedValue, setStoredValue] = useState<T>(() => {\n if (!isClient) {\n return resolveInitialValue(initialValue);\n }\n\n try {\n const item = window.sessionStorage.getItem(key);\n if (item !== null) {\n return deserializerRef.current(item);\n }\n return resolveInitialValue(initialValue);\n } catch (error) {\n onErrorRef.current?.(error as Error);\n return resolveInitialValue(initialValue);\n }\n });\n\n // Store current value in ref for stable setValue reference\n const storedValueRef = useRef<T>(storedValue);\n storedValueRef.current = storedValue;\n\n // setValue - stable reference (only depends on key)\n const setValue = useCallback<React.Dispatch<React.SetStateAction<T>>>(\n (value) => {\n try {\n const currentValue = storedValueRef.current;\n const valueToStore =\n value instanceof Function ? value(currentValue) : value;\n\n setStoredValue(valueToStore);\n\n if (typeof window !== \"undefined\") {\n window.sessionStorage.setItem(\n key,\n serializerRef.current(valueToStore)\n );\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 const initial = resolveInitialValue(initialValueRef.current);\n setStoredValue(initial);\n\n if (typeof window !== \"undefined\") {\n window.sessionStorage.removeItem(key);\n }\n } catch (error) {\n onErrorRef.current?.(error as Error);\n }\n }, [key]);\n\n // Re-read value when key changes\n useEffect(() => {\n if (!isClient) {\n return;\n }\n\n try {\n const item = window.sessionStorage.getItem(key);\n if (item !== null) {\n setStoredValue(deserializerRef.current(item));\n } else {\n setStoredValue(resolveInitialValue(initialValueRef.current));\n }\n } catch {\n setStoredValue(resolveInitialValue(initialValueRef.current));\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [key]);\n\n return [storedValue, setValue, removeValue] as const;\n}\n"],"mappings":";AAAA,SAAS,aAAa,WAAW,QAAQ,gBAAgB;AA0CzD,SAAS,oBAAuB,cAAkC;AAChE,SAAO,OAAO,iBAAiB,aAC1B,aAAyB,IAC1B;AACN;AA0DO,SAAS,kBACd,KACA,cACA,UAAuC,CAAC,GACZ;AAC5B,QAAM;AAAA,IACJ,aAAa,KAAK;AAAA,IAClB,eAAe,KAAK;AAAA,IACpB;AAAA,EACF,IAAI;AAGJ,QAAM,gBAAgB,OAAO,UAAU;AACvC,QAAM,kBAAkB,OAAO,YAAY;AAC3C,QAAM,aAAa,OAAO,OAAO;AACjC,gBAAc,UAAU;AACxB,kBAAgB,UAAU;AAC1B,aAAW,UAAU;AAGrB,QAAM,kBAAkB,OAAO,YAAY;AAC3C,kBAAgB,UAAU;AAG1B,QAAM,WAAW,OAAO,WAAW;AAGnC,QAAM,CAAC,aAAa,cAAc,IAAI,SAAY,MAAM;AACtD,QAAI,CAAC,UAAU;AACb,aAAO,oBAAoB,YAAY;AAAA,IACzC;AAEA,QAAI;AACF,YAAM,OAAO,OAAO,eAAe,QAAQ,GAAG;AAC9C,UAAI,SAAS,MAAM;AACjB,eAAO,gBAAgB,QAAQ,IAAI;AAAA,MACrC;AACA,aAAO,oBAAoB,YAAY;AAAA,IACzC,SAAS,OAAO;AACd,iBAAW,UAAU,KAAc;AACnC,aAAO,oBAAoB,YAAY;AAAA,IACzC;AAAA,EACF,CAAC;AAGD,QAAM,iBAAiB,OAAU,WAAW;AAC5C,iBAAe,UAAU;AAGzB,QAAM,WAAW;AAAA,IACf,CAAC,UAAU;AACT,UAAI;AACF,cAAM,eAAe,eAAe;AACpC,cAAM,eACJ,iBAAiB,WAAW,MAAM,YAAY,IAAI;AAEpD,uBAAe,YAAY;AAE3B,YAAI,OAAO,WAAW,aAAa;AACjC,iBAAO,eAAe;AAAA,YACpB;AAAA,YACA,cAAc,QAAQ,YAAY;AAAA,UACpC;AAAA,QACF;AAAA,MACF,SAAS,OAAO;AACd,mBAAW,UAAU,KAAc;AAAA,MACrC;AAAA,IACF;AAAA,IACA,CAAC,GAAG;AAAA,EACN;AAGA,QAAM,cAAc,YAAY,MAAM;AACpC,QAAI;AACF,YAAM,UAAU,oBAAoB,gBAAgB,OAAO;AAC3D,qBAAe,OAAO;AAEtB,UAAI,OAAO,WAAW,aAAa;AACjC,eAAO,eAAe,WAAW,GAAG;AAAA,MACtC;AAAA,IACF,SAAS,OAAO;AACd,iBAAW,UAAU,KAAc;AAAA,IACrC;AAAA,EACF,GAAG,CAAC,GAAG,CAAC;AAGR,YAAU,MAAM;AACd,QAAI,CAAC,UAAU;AACb;AAAA,IACF;AAEA,QAAI;AACF,YAAM,OAAO,OAAO,eAAe,QAAQ,GAAG;AAC9C,UAAI,SAAS,MAAM;AACjB,uBAAe,gBAAgB,QAAQ,IAAI,CAAC;AAAA,MAC9C,OAAO;AACL,uBAAe,oBAAoB,gBAAgB,OAAO,CAAC;AAAA,MAC7D;AAAA,IACF,QAAQ;AACN,qBAAe,oBAAoB,gBAAgB,OAAO,CAAC;AAAA,IAC7D;AAAA,EAEF,GAAG,CAAC,GAAG,CAAC;AAER,SAAO,CAAC,aAAa,UAAU,WAAW;AAC5C;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/useSessionStorage.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 useSessionStorage hook\n */\nexport interface UseSessionStorageOptions<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 * Callback function called when an error occurs\n */\n onError?: (error: Error) => void;\n}\n\n/**\n * Return type for useSessionStorage hook - tuple similar to useState\n */\nexport type UseSessionStorageReturn<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 sessionStorage */\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 sessionStorage with automatic synchronization.\n * Works like useState but persists the value in sessionStorage for the duration of the browser session.\n *\n * Features:\n * - Same-tab synchronization: Multiple components using the same key will stay in sync\n * - SSR compatible: Works with Next.js, Remix, and other SSR frameworks\n *\n * Unlike localStorage, sessionStorage data:\n * - Is cleared when the tab/window is closed\n * - Is not shared between tabs (each tab has its own session)\n *\n * @template T - The type of the stored value\n * @param key - The sessionStorage 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 and error handling\n * @returns A tuple of [storedValue, setValue, removeValue]\n *\n * @example\n * ```tsx\n * // Basic usage - form data that persists during session\n * function CheckoutForm() {\n * const [formData, setFormData, clearForm] = useSessionStorage('checkout-form', {\n * name: '',\n * email: '',\n * });\n *\n * return (\n * <form>\n * <input\n * value={formData.name}\n * onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}\n * />\n * <button type=\"button\" onClick={clearForm}>Clear</button>\n * </form>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Same-tab synchronization - both components stay in sync\n * function ComponentA() {\n * const [step, setStep] = useSessionStorage('wizard-step', 1);\n * return <button onClick={() => setStep(s => s + 1)}>Next Step</button>;\n * }\n *\n * function ComponentB() {\n * const [step] = useSessionStorage('wizard-step', 1);\n * // Automatically updates when ComponentA calls setStep!\n * return <p>Current Step: {step}</p>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Temporary state that resets on tab close\n * const [wizardStep, setWizardStep] = useSessionStorage('wizard-step', 1);\n * ```\n *\n * @example\n * ```tsx\n * // With lazy initialization\n * const [cache, setCache] = useSessionStorage('cache', () => computeInitialCache());\n * ```\n *\n * @example\n * ```tsx\n * // With custom serializer/deserializer\n * const [date, setDate] = useSessionStorage<Date>('lastAction', new Date(), {\n * serializer: (d) => d.toISOString(),\n * deserializer: (s) => new Date(s),\n * });\n * ```\n */\nexport function useSessionStorage<T>(\n key: string,\n initialValue: InitialValue<T>,\n options: UseSessionStorageOptions<T> = {}\n): UseSessionStorageReturn<T> {\n const {\n serializer = JSON.stringify,\n deserializer = JSON.parse,\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 same-tab synchronization (sessionStorage doesn't have cross-tab sync)\n const subscribeToStore = useCallback(\n (onStoreChange: () => void) => {\n // Subscribe to same-tab changes via internal store\n const unsubscribeStore = subscribe(key, onStoreChange);\n\n // Note: sessionStorage doesn't fire storage events for changes in the same tab,\n // and changes in other tabs don't affect this tab's sessionStorage.\n // So we only use the internal store for synchronization.\n\n return () => {\n unsubscribeStore();\n };\n },\n [key]\n );\n\n // getSnapshot: Read current value from sessionStorage with caching\n const getSnapshot = useCallback((): T => {\n if (!isClient) {\n return resolveInitialValue(initialValueRef.current);\n }\n\n try {\n const rawValue = window.sessionStorage.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 sessionStorage 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.sessionStorage.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.sessionStorage.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.sessionStorage.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 sessionStorage synchronization\n * This module manages listeners for same-tab synchronization across components\n * using the same sessionStorage 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 sessionStorage 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 sessionStorage 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":[]}
|