@usefy/use-debounce-callback 0.0.8 → 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 +476 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
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-debounce-callback</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>A powerful React hook for debounced callbacks with cancel, flush, and pending methods</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/@usefy/use-debounce-callback">
|
|
13
|
+
<img src="https://img.shields.io/npm/v/@usefy/use-debounce-callback.svg?style=flat-square&color=007acc" alt="npm version" />
|
|
14
|
+
</a>
|
|
15
|
+
<a href="https://www.npmjs.com/package/@usefy/use-debounce-callback">
|
|
16
|
+
<img src="https://img.shields.io/npm/dm/@usefy/use-debounce-callback.svg?style=flat-square&color=007acc" alt="npm downloads" />
|
|
17
|
+
</a>
|
|
18
|
+
<a href="https://bundlephobia.com/package/@usefy/use-debounce-callback">
|
|
19
|
+
<img src="https://img.shields.io/bundlephobia/minzip/@usefy/use-debounce-callback?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-debounce-callback.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-debounce-callback` provides a debounced version of your callback function with full control methods: `cancel()`, `flush()`, and `pending()`. Perfect for API calls, form submissions, event handlers, and any scenario requiring debounced function execution with fine-grained control.
|
|
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-debounce-callback?
|
|
43
|
+
|
|
44
|
+
- **Zero Dependencies** — Pure React implementation with no external dependencies
|
|
45
|
+
- **TypeScript First** — Full type safety with generics and exported interfaces
|
|
46
|
+
- **Full Control** — `cancel()`, `flush()`, and `pending()` methods
|
|
47
|
+
- **Flexible Options** — Leading edge, trailing edge, and maxWait support
|
|
48
|
+
- **SSR Compatible** — Works seamlessly with Next.js, Remix, and other SSR frameworks
|
|
49
|
+
- **Lightweight** — Minimal bundle footprint (~500B minified + gzipped)
|
|
50
|
+
- **Well Tested** — Comprehensive test coverage with Vitest
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# npm
|
|
58
|
+
npm install @usefy/use-debounce-callback
|
|
59
|
+
|
|
60
|
+
# yarn
|
|
61
|
+
yarn add @usefy/use-debounce-callback
|
|
62
|
+
|
|
63
|
+
# pnpm
|
|
64
|
+
pnpm add @usefy/use-debounce-callback
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Peer Dependencies
|
|
68
|
+
|
|
69
|
+
This package requires React 18 or 19:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Quick Start
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { useDebounceCallback } from '@usefy/use-debounce-callback';
|
|
85
|
+
|
|
86
|
+
function SearchInput() {
|
|
87
|
+
const [query, setQuery] = useState('');
|
|
88
|
+
|
|
89
|
+
const debouncedSearch = useDebounceCallback((searchTerm: string) => {
|
|
90
|
+
fetchSearchResults(searchTerm);
|
|
91
|
+
}, 300);
|
|
92
|
+
|
|
93
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
94
|
+
setQuery(e.target.value);
|
|
95
|
+
debouncedSearch(e.target.value);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<input
|
|
100
|
+
type="text"
|
|
101
|
+
value={query}
|
|
102
|
+
onChange={handleChange}
|
|
103
|
+
placeholder="Search..."
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## API Reference
|
|
112
|
+
|
|
113
|
+
### `useDebounceCallback<T>(callback, delay?, options?)`
|
|
114
|
+
|
|
115
|
+
A hook that returns a debounced version of the provided callback function.
|
|
116
|
+
|
|
117
|
+
#### Parameters
|
|
118
|
+
|
|
119
|
+
| Parameter | Type | Default | Description |
|
|
120
|
+
|-----------|------|---------|-------------|
|
|
121
|
+
| `callback` | `T extends (...args: any[]) => any` | — | The callback function to debounce |
|
|
122
|
+
| `delay` | `number` | `500` | The debounce delay in milliseconds |
|
|
123
|
+
| `options` | `UseDebounceCallbackOptions` | `{}` | Additional configuration options |
|
|
124
|
+
|
|
125
|
+
#### Options
|
|
126
|
+
|
|
127
|
+
| Option | Type | Default | Description |
|
|
128
|
+
|--------|------|---------|-------------|
|
|
129
|
+
| `leading` | `boolean` | `false` | Invoke on the leading edge (first call) |
|
|
130
|
+
| `trailing` | `boolean` | `true` | Invoke on the trailing edge (after delay) |
|
|
131
|
+
| `maxWait` | `number` | — | Maximum time to wait before forcing invocation |
|
|
132
|
+
|
|
133
|
+
#### Returns `DebouncedFunction<T>`
|
|
134
|
+
|
|
135
|
+
| Property | Type | Description |
|
|
136
|
+
|----------|------|-------------|
|
|
137
|
+
| `(...args)` | `ReturnType<T>` | The debounced function (same signature as original) |
|
|
138
|
+
| `cancel` | `() => void` | Cancels any pending invocation |
|
|
139
|
+
| `flush` | `() => void` | Immediately invokes any pending invocation |
|
|
140
|
+
| `pending` | `() => boolean` | Returns `true` if there's a pending invocation |
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Examples
|
|
145
|
+
|
|
146
|
+
### Auto-Save with Cancel
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
import { useDebounceCallback } from '@usefy/use-debounce-callback';
|
|
150
|
+
|
|
151
|
+
function Editor() {
|
|
152
|
+
const [content, setContent] = useState('');
|
|
153
|
+
|
|
154
|
+
const debouncedSave = useDebounceCallback((text: string) => {
|
|
155
|
+
saveToServer(text);
|
|
156
|
+
console.log('Auto-saved');
|
|
157
|
+
}, 1000);
|
|
158
|
+
|
|
159
|
+
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
160
|
+
setContent(e.target.value);
|
|
161
|
+
debouncedSave(e.target.value);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const handleManualSave = () => {
|
|
165
|
+
// Flush any pending save immediately
|
|
166
|
+
debouncedSave.flush();
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const handleDiscard = () => {
|
|
170
|
+
// Cancel pending save and reset content
|
|
171
|
+
debouncedSave.cancel();
|
|
172
|
+
setContent('');
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<div>
|
|
177
|
+
<textarea value={content} onChange={handleChange} />
|
|
178
|
+
<button onClick={handleManualSave}>Save Now</button>
|
|
179
|
+
<button onClick={handleDiscard}>Discard</button>
|
|
180
|
+
{debouncedSave.pending() && <span>Saving...</span>}
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Search with Immediate First Call
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
import { useDebounceCallback } from '@usefy/use-debounce-callback';
|
|
190
|
+
|
|
191
|
+
function SearchWithSuggestions() {
|
|
192
|
+
const [results, setResults] = useState([]);
|
|
193
|
+
|
|
194
|
+
// First keystroke triggers immediate search, then debounce
|
|
195
|
+
const debouncedSearch = useDebounceCallback(
|
|
196
|
+
async (query: string) => {
|
|
197
|
+
const data = await fetch(`/api/search?q=${query}`);
|
|
198
|
+
setResults(await data.json());
|
|
199
|
+
},
|
|
200
|
+
300,
|
|
201
|
+
{ leading: true }
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<input
|
|
206
|
+
type="text"
|
|
207
|
+
onChange={(e) => debouncedSearch(e.target.value)}
|
|
208
|
+
placeholder="Search..."
|
|
209
|
+
/>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Form Validation
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
import { useDebounceCallback } from '@usefy/use-debounce-callback';
|
|
218
|
+
|
|
219
|
+
function RegistrationForm() {
|
|
220
|
+
const [email, setEmail] = useState('');
|
|
221
|
+
const [error, setError] = useState('');
|
|
222
|
+
|
|
223
|
+
const validateEmail = useDebounceCallback(async (value: string) => {
|
|
224
|
+
if (!value.includes('@')) {
|
|
225
|
+
setError('Invalid email format');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const response = await fetch(`/api/check-email?e=${value}`);
|
|
229
|
+
const { available } = await response.json();
|
|
230
|
+
setError(available ? '' : 'Email already registered');
|
|
231
|
+
}, 500);
|
|
232
|
+
|
|
233
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
234
|
+
setEmail(e.target.value);
|
|
235
|
+
setError(''); // Clear error immediately
|
|
236
|
+
validateEmail(e.target.value);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<div>
|
|
241
|
+
<input
|
|
242
|
+
type="email"
|
|
243
|
+
value={email}
|
|
244
|
+
onChange={handleChange}
|
|
245
|
+
placeholder="Enter email"
|
|
246
|
+
/>
|
|
247
|
+
{error && <span className="error">{error}</span>}
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Event Handler with maxWait
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
import { useDebounceCallback } from '@usefy/use-debounce-callback';
|
|
257
|
+
|
|
258
|
+
function ResizeHandler() {
|
|
259
|
+
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
|
260
|
+
|
|
261
|
+
// Debounce resize events, but guarantee update every 1 second
|
|
262
|
+
const handleResize = useDebounceCallback(
|
|
263
|
+
() => {
|
|
264
|
+
setDimensions({
|
|
265
|
+
width: window.innerWidth,
|
|
266
|
+
height: window.innerHeight,
|
|
267
|
+
});
|
|
268
|
+
},
|
|
269
|
+
250,
|
|
270
|
+
{ maxWait: 1000 }
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
window.addEventListener('resize', handleResize);
|
|
275
|
+
return () => {
|
|
276
|
+
handleResize.cancel();
|
|
277
|
+
window.removeEventListener('resize', handleResize);
|
|
278
|
+
};
|
|
279
|
+
}, [handleResize]);
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<div>
|
|
283
|
+
Window: {dimensions.width} x {dimensions.height}
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### API Request with Pending State
|
|
290
|
+
|
|
291
|
+
```tsx
|
|
292
|
+
import { useDebounceCallback } from '@usefy/use-debounce-callback';
|
|
293
|
+
|
|
294
|
+
function DataFetcher() {
|
|
295
|
+
const [data, setData] = useState(null);
|
|
296
|
+
const [loading, setLoading] = useState(false);
|
|
297
|
+
|
|
298
|
+
const fetchData = useDebounceCallback(
|
|
299
|
+
async (params: QueryParams) => {
|
|
300
|
+
setLoading(true);
|
|
301
|
+
try {
|
|
302
|
+
const response = await fetch('/api/data', {
|
|
303
|
+
method: 'POST',
|
|
304
|
+
body: JSON.stringify(params),
|
|
305
|
+
});
|
|
306
|
+
setData(await response.json());
|
|
307
|
+
} finally {
|
|
308
|
+
setLoading(false);
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
500
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<div>
|
|
316
|
+
<button onClick={() => fetchData({ page: 1 })}>
|
|
317
|
+
{fetchData.pending() ? 'Request pending...' : 'Fetch Data'}
|
|
318
|
+
</button>
|
|
319
|
+
{loading && <Spinner />}
|
|
320
|
+
</div>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Cleanup on Unmount
|
|
326
|
+
|
|
327
|
+
```tsx
|
|
328
|
+
import { useDebounceCallback } from '@usefy/use-debounce-callback';
|
|
329
|
+
|
|
330
|
+
function Component() {
|
|
331
|
+
const debouncedAction = useDebounceCallback(() => {
|
|
332
|
+
// Some action
|
|
333
|
+
}, 500);
|
|
334
|
+
|
|
335
|
+
// Cancel pending on unmount
|
|
336
|
+
useEffect(() => {
|
|
337
|
+
return () => {
|
|
338
|
+
debouncedAction.cancel();
|
|
339
|
+
};
|
|
340
|
+
}, [debouncedAction]);
|
|
341
|
+
|
|
342
|
+
return <button onClick={debouncedAction}>Action</button>;
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## TypeScript
|
|
349
|
+
|
|
350
|
+
This hook is written in TypeScript with full generic support.
|
|
351
|
+
|
|
352
|
+
```tsx
|
|
353
|
+
import {
|
|
354
|
+
useDebounceCallback,
|
|
355
|
+
type UseDebounceCallbackOptions,
|
|
356
|
+
type DebouncedFunction,
|
|
357
|
+
} from '@usefy/use-debounce-callback';
|
|
358
|
+
|
|
359
|
+
// Type inference from callback
|
|
360
|
+
const debouncedFn = useDebounceCallback((a: string, b: number) => {
|
|
361
|
+
return `${a}-${b}`;
|
|
362
|
+
}, 300);
|
|
363
|
+
|
|
364
|
+
// debouncedFn(string, number) => string | undefined
|
|
365
|
+
// debouncedFn.cancel() => void
|
|
366
|
+
// debouncedFn.flush() => void
|
|
367
|
+
// debouncedFn.pending() => boolean
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Testing
|
|
373
|
+
|
|
374
|
+
This package maintains comprehensive test coverage to ensure reliability and stability.
|
|
375
|
+
|
|
376
|
+
### Test Coverage
|
|
377
|
+
|
|
378
|
+
| Category | Tests | Coverage |
|
|
379
|
+
|----------|-------|----------|
|
|
380
|
+
| Initialization | 4 | 100% |
|
|
381
|
+
| Basic Debouncing | 6 | 100% |
|
|
382
|
+
| Leading Edge | 5 | 100% |
|
|
383
|
+
| Trailing Edge | 3 | 100% |
|
|
384
|
+
| maxWait Option | 4 | 100% |
|
|
385
|
+
| cancel Method | 4 | 100% |
|
|
386
|
+
| flush Method | 4 | 100% |
|
|
387
|
+
| pending Method | 4 | 100% |
|
|
388
|
+
| Callback Updates | 3 | 100% |
|
|
389
|
+
| Cleanup | 2 | 100% |
|
|
390
|
+
| **Total** | **39** | **94.05%** |
|
|
391
|
+
|
|
392
|
+
### Test Categories
|
|
393
|
+
|
|
394
|
+
<details>
|
|
395
|
+
<summary><strong>Control Method Tests</strong></summary>
|
|
396
|
+
|
|
397
|
+
- Cancel pending invocations
|
|
398
|
+
- Flush immediately invokes pending callback
|
|
399
|
+
- pending() returns correct state
|
|
400
|
+
- cancel() clears pending state
|
|
401
|
+
- flush() clears pending state after invocation
|
|
402
|
+
|
|
403
|
+
</details>
|
|
404
|
+
|
|
405
|
+
<details>
|
|
406
|
+
<summary><strong>Leading/Trailing Edge Tests</strong></summary>
|
|
407
|
+
|
|
408
|
+
- Invoke on leading edge with leading: true
|
|
409
|
+
- No immediate invoke with leading: false (default)
|
|
410
|
+
- Invoke on trailing edge with trailing: true (default)
|
|
411
|
+
- No trailing invoke with trailing: false
|
|
412
|
+
- Combined leading and trailing options
|
|
413
|
+
|
|
414
|
+
</details>
|
|
415
|
+
|
|
416
|
+
### Running Tests
|
|
417
|
+
|
|
418
|
+
```bash
|
|
419
|
+
# Run all tests
|
|
420
|
+
pnpm test
|
|
421
|
+
|
|
422
|
+
# Run tests in watch mode
|
|
423
|
+
pnpm test:watch
|
|
424
|
+
|
|
425
|
+
# Run tests with coverage report
|
|
426
|
+
pnpm test --coverage
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## Related Packages
|
|
432
|
+
|
|
433
|
+
Explore other hooks in the **@usefy** collection:
|
|
434
|
+
|
|
435
|
+
| Package | Description |
|
|
436
|
+
|---------|-------------|
|
|
437
|
+
| [@usefy/use-debounce](https://www.npmjs.com/package/@usefy/use-debounce) | Value debouncing |
|
|
438
|
+
| [@usefy/use-throttle](https://www.npmjs.com/package/@usefy/use-throttle) | Value throttling |
|
|
439
|
+
| [@usefy/use-throttle-callback](https://www.npmjs.com/package/@usefy/use-throttle-callback) | Throttled callbacks |
|
|
440
|
+
| [@usefy/use-toggle](https://www.npmjs.com/package/@usefy/use-toggle) | Boolean state management |
|
|
441
|
+
| [@usefy/use-counter](https://www.npmjs.com/package/@usefy/use-counter) | Counter state management |
|
|
442
|
+
| [@usefy/use-click-any-where](https://www.npmjs.com/package/@usefy/use-click-any-where) | Global click detection |
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Contributing
|
|
447
|
+
|
|
448
|
+
We welcome contributions! Please see our [Contributing Guide](https://github.com/geon0529/usefy/blob/master/CONTRIBUTING.md) for details.
|
|
449
|
+
|
|
450
|
+
```bash
|
|
451
|
+
# Clone the repository
|
|
452
|
+
git clone https://github.com/geon0529/usefy.git
|
|
453
|
+
|
|
454
|
+
# Install dependencies
|
|
455
|
+
pnpm install
|
|
456
|
+
|
|
457
|
+
# Run tests
|
|
458
|
+
pnpm test
|
|
459
|
+
|
|
460
|
+
# Build
|
|
461
|
+
pnpm build
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## License
|
|
467
|
+
|
|
468
|
+
MIT © [mirunamu](https://github.com/geon0529)
|
|
469
|
+
|
|
470
|
+
This package is part of the [usefy](https://github.com/geon0529/usefy) monorepo.
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
<p align="center">
|
|
475
|
+
<sub>Built with care by the usefy team</sub>
|
|
476
|
+
</p>
|