@usefy/use-session-storage 0.0.7 → 0.0.10
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 +562 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/geon0529/usefy/master/assets/logo.png" alt="usefy logo" width="120" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">@usefy/use-session-storage</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>A lightweight React hook for persisting state in sessionStorage</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/@usefy/use-session-storage">
|
|
13
|
+
<img src="https://img.shields.io/npm/v/@usefy/use-session-storage.svg?style=flat-square&color=007acc" alt="npm version" />
|
|
14
|
+
</a>
|
|
15
|
+
<a href="https://www.npmjs.com/package/@usefy/use-session-storage">
|
|
16
|
+
<img src="https://img.shields.io/npm/dm/@usefy/use-session-storage.svg?style=flat-square&color=007acc" alt="npm downloads" />
|
|
17
|
+
</a>
|
|
18
|
+
<a href="https://bundlephobia.com/package/@usefy/use-session-storage">
|
|
19
|
+
<img src="https://img.shields.io/bundlephobia/minzip/@usefy/use-session-storage?style=flat-square&color=007acc" alt="bundle size" />
|
|
20
|
+
</a>
|
|
21
|
+
<a href="https://github.com/geon0529/usefy/blob/master/LICENSE">
|
|
22
|
+
<img src="https://img.shields.io/npm/l/@usefy/use-session-storage.svg?style=flat-square&color=007acc" alt="license" />
|
|
23
|
+
</a>
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<a href="#installation">Installation</a> •
|
|
28
|
+
<a href="#quick-start">Quick Start</a> •
|
|
29
|
+
<a href="#api-reference">API Reference</a> •
|
|
30
|
+
<a href="#examples">Examples</a> •
|
|
31
|
+
<a href="#license">License</a>
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Overview
|
|
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.
|
|
39
|
+
|
|
40
|
+
**Part of the [@usefy](https://www.npmjs.com/org/usefy) ecosystem** — a collection of production-ready React hooks designed for modern applications.
|
|
41
|
+
|
|
42
|
+
### Why use-session-storage?
|
|
43
|
+
|
|
44
|
+
- **Zero Dependencies** — Pure React implementation with no external dependencies
|
|
45
|
+
- **TypeScript First** — Full type safety with generics and exported interfaces
|
|
46
|
+
- **useState-like API** — Familiar tuple return: `[value, setValue, removeValue]`
|
|
47
|
+
- **Tab Isolation** — Each browser tab has its own session storage
|
|
48
|
+
- **Auto-Cleanup** — Data cleared automatically when tab closes
|
|
49
|
+
- **Custom Serialization** — Support for Date, Map, Set, or any custom type
|
|
50
|
+
- **Lazy Initialization** — Function initializer support for expensive defaults
|
|
51
|
+
- **Error Handling** — `onError` callback for graceful error recovery
|
|
52
|
+
- **SSR Compatible** — Works seamlessly with Next.js, Remix, and other SSR frameworks
|
|
53
|
+
- **Stable References** — Memoized functions for optimal performance
|
|
54
|
+
- **Well Tested** — Comprehensive test coverage with Vitest
|
|
55
|
+
|
|
56
|
+
### localStorage vs sessionStorage
|
|
57
|
+
|
|
58
|
+
| Feature | localStorage | sessionStorage |
|
|
59
|
+
|---------|--------------|----------------|
|
|
60
|
+
| Data persistence | Until explicitly cleared | Until tab closes |
|
|
61
|
+
| Tab sharing | Shared across all tabs | Isolated per tab |
|
|
62
|
+
| Best for | User preferences, themes | Form drafts, wizard steps |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Installation
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# npm
|
|
70
|
+
npm install @usefy/use-session-storage
|
|
71
|
+
|
|
72
|
+
# yarn
|
|
73
|
+
yarn add @usefy/use-session-storage
|
|
74
|
+
|
|
75
|
+
# pnpm
|
|
76
|
+
pnpm add @usefy/use-session-storage
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Peer Dependencies
|
|
80
|
+
|
|
81
|
+
This package requires React 18 or 19:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"peerDependencies": {
|
|
86
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Quick Start
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import { useSessionStorage } from '@usefy/use-session-storage';
|
|
97
|
+
|
|
98
|
+
function CheckoutForm() {
|
|
99
|
+
const [formData, setFormData, clearForm] = useSessionStorage('checkout-form', {
|
|
100
|
+
name: '',
|
|
101
|
+
email: '',
|
|
102
|
+
address: '',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<form>
|
|
107
|
+
<input
|
|
108
|
+
value={formData.name}
|
|
109
|
+
onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
|
|
110
|
+
placeholder="Name"
|
|
111
|
+
/>
|
|
112
|
+
<input
|
|
113
|
+
value={formData.email}
|
|
114
|
+
onChange={(e) => setFormData((prev) => ({ ...prev, email: e.target.value }))}
|
|
115
|
+
placeholder="Email"
|
|
116
|
+
/>
|
|
117
|
+
<button type="button" onClick={clearForm}>Clear Form</button>
|
|
118
|
+
</form>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## API Reference
|
|
126
|
+
|
|
127
|
+
### `useSessionStorage<T>(key, initialValue, options?)`
|
|
128
|
+
|
|
129
|
+
A hook that persists state in sessionStorage for the duration of the browser session.
|
|
130
|
+
|
|
131
|
+
#### Parameters
|
|
132
|
+
|
|
133
|
+
| Parameter | Type | Description |
|
|
134
|
+
|-----------|------|-------------|
|
|
135
|
+
| `key` | `string` | The sessionStorage key |
|
|
136
|
+
| `initialValue` | `T \| () => T` | Initial value or lazy initializer function |
|
|
137
|
+
| `options` | `UseSessionStorageOptions<T>` | Configuration options |
|
|
138
|
+
|
|
139
|
+
#### Options
|
|
140
|
+
|
|
141
|
+
| Option | Type | Default | Description |
|
|
142
|
+
|--------|------|---------|-------------|
|
|
143
|
+
| `serializer` | `(value: T) => string` | `JSON.stringify` | Custom serializer function |
|
|
144
|
+
| `deserializer` | `(value: string) => T` | `JSON.parse` | Custom deserializer function |
|
|
145
|
+
| `onError` | `(error: Error) => void` | — | Callback for error handling |
|
|
146
|
+
|
|
147
|
+
#### Returns `[T, SetValue<T>, RemoveValue]`
|
|
148
|
+
|
|
149
|
+
| Index | Type | Description |
|
|
150
|
+
|-------|------|-------------|
|
|
151
|
+
| `[0]` | `T` | Current stored value |
|
|
152
|
+
| `[1]` | `Dispatch<SetStateAction<T>>` | Function to update value (same as useState) |
|
|
153
|
+
| `[2]` | `() => void` | Function to remove value and reset to initial |
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Examples
|
|
158
|
+
|
|
159
|
+
### Multi-Step Wizard
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
import { useSessionStorage } from '@usefy/use-session-storage';
|
|
163
|
+
|
|
164
|
+
function SignupWizard() {
|
|
165
|
+
const [step, setStep] = useSessionStorage('signup-step', 1);
|
|
166
|
+
const [formData, setFormData, resetForm] = useSessionStorage('signup-data', {
|
|
167
|
+
email: '',
|
|
168
|
+
password: '',
|
|
169
|
+
profile: {},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const handleNext = () => setStep((prev) => prev + 1);
|
|
173
|
+
const handleBack = () => setStep((prev) => prev - 1);
|
|
174
|
+
|
|
175
|
+
const handleComplete = async () => {
|
|
176
|
+
await submitSignup(formData);
|
|
177
|
+
resetForm();
|
|
178
|
+
setStep(1);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<div>
|
|
183
|
+
<p>Step {step} of 3</p>
|
|
184
|
+
|
|
185
|
+
{step === 1 && (
|
|
186
|
+
<EmailStep
|
|
187
|
+
value={formData.email}
|
|
188
|
+
onChange={(email) => setFormData((prev) => ({ ...prev, email }))}
|
|
189
|
+
onNext={handleNext}
|
|
190
|
+
/>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
{step === 2 && (
|
|
194
|
+
<PasswordStep
|
|
195
|
+
value={formData.password}
|
|
196
|
+
onChange={(password) => setFormData((prev) => ({ ...prev, password }))}
|
|
197
|
+
onBack={handleBack}
|
|
198
|
+
onNext={handleNext}
|
|
199
|
+
/>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
{step === 3 && (
|
|
203
|
+
<ProfileStep
|
|
204
|
+
value={formData.profile}
|
|
205
|
+
onChange={(profile) => setFormData((prev) => ({ ...prev, profile }))}
|
|
206
|
+
onBack={handleBack}
|
|
207
|
+
onComplete={handleComplete}
|
|
208
|
+
/>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Form Draft (Auto-Restore)
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
import { useSessionStorage } from '@usefy/use-session-storage';
|
|
219
|
+
|
|
220
|
+
function ContactForm() {
|
|
221
|
+
const [draft, setDraft, clearDraft] = useSessionStorage('contact-draft', {
|
|
222
|
+
subject: '',
|
|
223
|
+
message: '',
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
227
|
+
e.preventDefault();
|
|
228
|
+
await sendMessage(draft);
|
|
229
|
+
clearDraft(); // Clear after successful submit
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<form onSubmit={handleSubmit}>
|
|
234
|
+
<input
|
|
235
|
+
value={draft.subject}
|
|
236
|
+
onChange={(e) => setDraft((prev) => ({ ...prev, subject: e.target.value }))}
|
|
237
|
+
placeholder="Subject"
|
|
238
|
+
/>
|
|
239
|
+
<textarea
|
|
240
|
+
value={draft.message}
|
|
241
|
+
onChange={(e) => setDraft((prev) => ({ ...prev, message: e.target.value }))}
|
|
242
|
+
placeholder="Message"
|
|
243
|
+
/>
|
|
244
|
+
<p className="hint">Your draft is auto-saved in this tab</p>
|
|
245
|
+
<button type="submit">Send</button>
|
|
246
|
+
<button type="button" onClick={clearDraft}>Discard</button>
|
|
247
|
+
</form>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Shopping Cart (Per-Tab)
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
import { useSessionStorage } from '@usefy/use-session-storage';
|
|
256
|
+
|
|
257
|
+
interface CartItem {
|
|
258
|
+
id: string;
|
|
259
|
+
name: string;
|
|
260
|
+
quantity: number;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function TabCart() {
|
|
264
|
+
const [cart, setCart, clearCart] = useSessionStorage<CartItem[]>('tab-cart', []);
|
|
265
|
+
|
|
266
|
+
const addItem = (product: Product) => {
|
|
267
|
+
setCart((prev) => {
|
|
268
|
+
const existing = prev.find((item) => item.id === product.id);
|
|
269
|
+
if (existing) {
|
|
270
|
+
return prev.map((item) =>
|
|
271
|
+
item.id === product.id
|
|
272
|
+
? { ...item, quantity: item.quantity + 1 }
|
|
273
|
+
: item
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
return [...prev, { id: product.id, name: product.name, quantity: 1 }];
|
|
277
|
+
});
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<div>
|
|
282
|
+
<p>Cart items: {cart.length}</p>
|
|
283
|
+
<p className="hint">This cart is specific to this tab only</p>
|
|
284
|
+
<button onClick={clearCart}>Clear Cart</button>
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Temporary Auth Token
|
|
291
|
+
|
|
292
|
+
```tsx
|
|
293
|
+
import { useSessionStorage } from '@usefy/use-session-storage';
|
|
294
|
+
|
|
295
|
+
function ProtectedPage() {
|
|
296
|
+
const [token, setToken, clearToken] = useSessionStorage<string | null>('auth-token', null);
|
|
297
|
+
|
|
298
|
+
const login = async (credentials: Credentials) => {
|
|
299
|
+
const response = await authenticate(credentials);
|
|
300
|
+
setToken(response.token);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const logout = () => {
|
|
304
|
+
clearToken();
|
|
305
|
+
// Token is automatically cleared when tab closes
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
if (!token) {
|
|
309
|
+
return <LoginForm onLogin={login} />;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<div>
|
|
314
|
+
<p>You are logged in (this session only)</p>
|
|
315
|
+
<button onClick={logout}>Logout</button>
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Custom Serialization (Date)
|
|
322
|
+
|
|
323
|
+
```tsx
|
|
324
|
+
import { useSessionStorage } from '@usefy/use-session-storage';
|
|
325
|
+
|
|
326
|
+
function SessionTimer() {
|
|
327
|
+
const [sessionStart] = useSessionStorage<Date>(
|
|
328
|
+
'session-start',
|
|
329
|
+
new Date(),
|
|
330
|
+
{
|
|
331
|
+
serializer: (date) => date.toISOString(),
|
|
332
|
+
deserializer: (str) => new Date(str),
|
|
333
|
+
}
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const [elapsed, setElapsed] = useState(0);
|
|
337
|
+
|
|
338
|
+
useEffect(() => {
|
|
339
|
+
const interval = setInterval(() => {
|
|
340
|
+
setElapsed(Math.floor((Date.now() - sessionStart.getTime()) / 1000));
|
|
341
|
+
}, 1000);
|
|
342
|
+
return () => clearInterval(interval);
|
|
343
|
+
}, [sessionStart]);
|
|
344
|
+
|
|
345
|
+
return <div>Session duration: {elapsed} seconds</div>;
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Error Handling
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
import { useSessionStorage } from '@usefy/use-session-storage';
|
|
353
|
+
|
|
354
|
+
function RobustSessionStorage() {
|
|
355
|
+
const [data, setData] = useSessionStorage('session-data', { items: [] }, {
|
|
356
|
+
onError: (error) => {
|
|
357
|
+
console.error('Session storage error:', error.message);
|
|
358
|
+
toast.error('Failed to save session data');
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
return <DataEditor data={data} onChange={setData} />;
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Lazy Initialization
|
|
367
|
+
|
|
368
|
+
```tsx
|
|
369
|
+
import { useSessionStorage } from '@usefy/use-session-storage';
|
|
370
|
+
|
|
371
|
+
function ExpensiveDefaultDemo() {
|
|
372
|
+
// Expensive computation only runs if no stored value exists
|
|
373
|
+
const [cache, setCache] = useSessionStorage('session-cache', () => {
|
|
374
|
+
console.log('Building initial cache...');
|
|
375
|
+
return buildExpensiveCache();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
return <CacheViewer cache={cache} />;
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Quiz Progress
|
|
383
|
+
|
|
384
|
+
```tsx
|
|
385
|
+
import { useSessionStorage } from '@usefy/use-session-storage';
|
|
386
|
+
|
|
387
|
+
interface QuizState {
|
|
388
|
+
currentQuestion: number;
|
|
389
|
+
answers: Record<number, string>;
|
|
390
|
+
startTime: number;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function Quiz() {
|
|
394
|
+
const [quiz, setQuiz, resetQuiz] = useSessionStorage<QuizState>('quiz-progress', {
|
|
395
|
+
currentQuestion: 0,
|
|
396
|
+
answers: {},
|
|
397
|
+
startTime: Date.now(),
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const submitAnswer = (answer: string) => {
|
|
401
|
+
setQuiz((prev) => ({
|
|
402
|
+
...prev,
|
|
403
|
+
answers: { ...prev.answers, [prev.currentQuestion]: answer },
|
|
404
|
+
currentQuestion: prev.currentQuestion + 1,
|
|
405
|
+
}));
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const handleComplete = async () => {
|
|
409
|
+
await submitQuiz(quiz.answers);
|
|
410
|
+
resetQuiz();
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
return (
|
|
414
|
+
<div>
|
|
415
|
+
<p>Question {quiz.currentQuestion + 1} of 10</p>
|
|
416
|
+
<QuestionCard
|
|
417
|
+
question={questions[quiz.currentQuestion]}
|
|
418
|
+
onAnswer={submitAnswer}
|
|
419
|
+
/>
|
|
420
|
+
<button onClick={resetQuiz}>Restart Quiz</button>
|
|
421
|
+
</div>
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
## TypeScript
|
|
429
|
+
|
|
430
|
+
This hook is written in TypeScript with full generic support.
|
|
431
|
+
|
|
432
|
+
```tsx
|
|
433
|
+
import {
|
|
434
|
+
useSessionStorage,
|
|
435
|
+
type UseSessionStorageOptions,
|
|
436
|
+
type UseSessionStorageReturn,
|
|
437
|
+
type InitialValue,
|
|
438
|
+
} from '@usefy/use-session-storage';
|
|
439
|
+
|
|
440
|
+
// Generic type inference
|
|
441
|
+
const [name, setName] = useSessionStorage('name', 'Guest'); // string
|
|
442
|
+
const [step, setStep] = useSessionStorage('step', 1); // number
|
|
443
|
+
const [items, setItems] = useSessionStorage('items', ['a']); // string[]
|
|
444
|
+
|
|
445
|
+
// Explicit generic type
|
|
446
|
+
interface FormData {
|
|
447
|
+
email: string;
|
|
448
|
+
message: string;
|
|
449
|
+
}
|
|
450
|
+
const [form, setForm] = useSessionStorage<FormData>('form', {
|
|
451
|
+
email: '',
|
|
452
|
+
message: '',
|
|
453
|
+
});
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## Testing
|
|
459
|
+
|
|
460
|
+
This package maintains comprehensive test coverage to ensure reliability and stability.
|
|
461
|
+
|
|
462
|
+
### Test Coverage
|
|
463
|
+
|
|
464
|
+
| Category | Tests | Coverage |
|
|
465
|
+
|----------|-------|----------|
|
|
466
|
+
| Initialization | 6 | 100% |
|
|
467
|
+
| setValue | 5 | 100% |
|
|
468
|
+
| removeValue | 2 | 100% |
|
|
469
|
+
| Type Preservation | 5 | 100% |
|
|
470
|
+
| Custom Serialization | 2 | 100% |
|
|
471
|
+
| Key Changes | 2 | 100% |
|
|
472
|
+
| Function Stability | 3 | 100% |
|
|
473
|
+
| Multiple Instances | 1 | 100% |
|
|
474
|
+
| Edge Cases | 6 | 100% |
|
|
475
|
+
| **Total** | **32** | **93.75%** |
|
|
476
|
+
|
|
477
|
+
### Test Categories
|
|
478
|
+
|
|
479
|
+
<details>
|
|
480
|
+
<summary><strong>Initialization Tests</strong></summary>
|
|
481
|
+
|
|
482
|
+
- Return initial value when sessionStorage is empty
|
|
483
|
+
- Return stored value when sessionStorage has data
|
|
484
|
+
- Support lazy initialization with function
|
|
485
|
+
- Not call initializer when sessionStorage has data
|
|
486
|
+
- Fallback to initial value when JSON parse fails
|
|
487
|
+
- Call onError when sessionStorage read fails
|
|
488
|
+
|
|
489
|
+
</details>
|
|
490
|
+
|
|
491
|
+
<details>
|
|
492
|
+
<summary><strong>setValue Tests</strong></summary>
|
|
493
|
+
|
|
494
|
+
- Update value and sessionStorage
|
|
495
|
+
- Support functional updates
|
|
496
|
+
- Handle object values
|
|
497
|
+
- Handle array values
|
|
498
|
+
- Call onError when write fails
|
|
499
|
+
|
|
500
|
+
</details>
|
|
501
|
+
|
|
502
|
+
### Running Tests
|
|
503
|
+
|
|
504
|
+
```bash
|
|
505
|
+
# Run all tests
|
|
506
|
+
pnpm test
|
|
507
|
+
|
|
508
|
+
# Run tests in watch mode
|
|
509
|
+
pnpm test:watch
|
|
510
|
+
|
|
511
|
+
# Run tests with coverage report
|
|
512
|
+
pnpm test --coverage
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## Related Packages
|
|
518
|
+
|
|
519
|
+
Explore other hooks in the **@usefy** collection:
|
|
520
|
+
|
|
521
|
+
| Package | Description |
|
|
522
|
+
|---------|-------------|
|
|
523
|
+
| [@usefy/use-local-storage](https://www.npmjs.com/package/@usefy/use-local-storage) | Persistent localStorage |
|
|
524
|
+
| [@usefy/use-toggle](https://www.npmjs.com/package/@usefy/use-toggle) | Boolean state management |
|
|
525
|
+
| [@usefy/use-counter](https://www.npmjs.com/package/@usefy/use-counter) | Counter state management |
|
|
526
|
+
| [@usefy/use-debounce](https://www.npmjs.com/package/@usefy/use-debounce) | Value debouncing |
|
|
527
|
+
| [@usefy/use-throttle](https://www.npmjs.com/package/@usefy/use-throttle) | Value throttling |
|
|
528
|
+
| [@usefy/use-click-any-where](https://www.npmjs.com/package/@usefy/use-click-any-where) | Global click detection |
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## Contributing
|
|
533
|
+
|
|
534
|
+
We welcome contributions! Please see our [Contributing Guide](https://github.com/geon0529/usefy/blob/master/CONTRIBUTING.md) for details.
|
|
535
|
+
|
|
536
|
+
```bash
|
|
537
|
+
# Clone the repository
|
|
538
|
+
git clone https://github.com/geon0529/usefy.git
|
|
539
|
+
|
|
540
|
+
# Install dependencies
|
|
541
|
+
pnpm install
|
|
542
|
+
|
|
543
|
+
# Run tests
|
|
544
|
+
pnpm test
|
|
545
|
+
|
|
546
|
+
# Build
|
|
547
|
+
pnpm build
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
---
|
|
551
|
+
|
|
552
|
+
## License
|
|
553
|
+
|
|
554
|
+
MIT © [mirunamu](https://github.com/geon0529)
|
|
555
|
+
|
|
556
|
+
This package is part of the [usefy](https://github.com/geon0529/usefy) monorepo.
|
|
557
|
+
|
|
558
|
+
---
|
|
559
|
+
|
|
560
|
+
<p align="center">
|
|
561
|
+
<sub>Built with care by the usefy team</sub>
|
|
562
|
+
</p>
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/useSessionStorage.ts"],"sourcesContent":["export {\
|
|
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":[]}
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/useSessionStorage.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\
|
|
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":[]}
|