@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 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
- | Category | Coverage |
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 [storedValue, setStoredValue] = (0, import_react.useState)(() => {
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(initialValue);
85
+ return resolveInitialValue(initialValueRef.current);
50
86
  }
51
87
  try {
52
- const item = window.sessionStorage.getItem(key);
53
- if (item !== null) {
54
- return deserializerRef.current(item);
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
- return resolveInitialValue(initialValue);
98
+ cacheRef.current = { rawValue, parsedValue };
99
+ return parsedValue;
57
100
  } catch (error) {
58
101
  onErrorRef.current?.(error);
59
- return resolveInitialValue(initialValue);
102
+ const fallbackValue = resolveInitialValue(initialValueRef.current);
103
+ cacheRef.current = { rawValue: null, parsedValue: fallbackValue };
104
+ return fallbackValue;
60
105
  }
61
- });
62
- const storedValueRef = (0, import_react.useRef)(storedValue);
63
- storedValueRef.current = storedValue;
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 = storedValueRef.current;
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
- window.sessionStorage.setItem(
72
- key,
73
- serializerRef.current(valueToStore)
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, useEffect, useRef, useState } from "react";
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 [storedValue, setStoredValue] = useState(() => {
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(initialValue);
59
+ return resolveInitialValue(initialValueRef.current);
24
60
  }
25
61
  try {
26
- const item = window.sessionStorage.getItem(key);
27
- if (item !== null) {
28
- return deserializerRef.current(item);
62
+ const rawValue = window.sessionStorage.getItem(key);
63
+ if (cacheRef.current && cacheRef.current.rawValue === rawValue) {
64
+ return cacheRef.current.parsedValue;
29
65
  }
30
- return resolveInitialValue(initialValue);
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
- return resolveInitialValue(initialValue);
76
+ const fallbackValue = resolveInitialValue(initialValueRef.current);
77
+ cacheRef.current = { rawValue: null, parsedValue: fallbackValue };
78
+ return fallbackValue;
34
79
  }
35
- });
36
- const storedValueRef = useRef(storedValue);
37
- storedValueRef.current = storedValue;
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 = storedValueRef.current;
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
- window.sessionStorage.setItem(
46
- key,
47
- serializerRef.current(valueToStore)
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 {
@@ -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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefy/use-session-storage",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "description": "A React hook for persisting state in sessionStorage",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",