@usefy/use-session-storage 0.0.8 → 0.0.11
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/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>
|