├── .eslintrc
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ └── main.yml
├── .gitignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── jest.config.js
├── logo.png
├── package-lock.json
├── package.json
├── setupTests.ts
├── src
├── index.ts
├── useDebounce.ts
├── useDebouncedCallback.ts
└── useThrottledCallback.ts
├── test
├── useDebounce.test.tsx
├── useDebouncedCallback.test.tsx
└── useThrottledCallback.test.tsx
└── tsconfig.json
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": [
4 | "plugin:@typescript-eslint/recommended",
5 | "prettier",
6 | "plugin:prettier/recommended"
7 | ],
8 | "parserOptions": {
9 | "ecmaVersion": 2018,
10 | "sourceType": "module"
11 | },
12 | "plugins": ["react-hooks"],
13 | "rules": {
14 | "react-hooks/rules-of-hooks": "error",
15 | "react-hooks/exhaustive-deps": "error",
16 | "semi": ["error", "always"],
17 | "space-before-function-paren": 0,
18 | "@typescript-eslint/explicit-function-return-type": 0,
19 | "@typescript-eslint/no-explicit-any": 0
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior.
15 |
16 | **Repro example**
17 | Please include links to codesandbox or other code sharing resource, so that we can reproduce & triage the issue quickly
18 |
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **use-debounce version:**
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe your idea**
11 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - master
7 | push:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v1
17 | - name: Use Node.js 21.x
18 | uses: actions/setup-node@v1
19 | with:
20 | node-version: 21.x
21 | - name: install and tests
22 | run: |
23 | npm install
24 | npm run test
25 | npm run size
26 | env:
27 | CI: true
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .vscode
3 | examples/bundle.js
4 | node_modules
5 | npm-debug.js
6 | esm
7 | lib
8 | .git
9 | .DS_store
10 | coverage
11 | tmp
12 | yarn-error.log
13 | dist
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 2,
4 | "singleQuote": true,
5 | "trailingComma": "es5",
6 | "arrowParens": "always"
7 | }
8 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 10.0.4
2 | - Fix behaviour for strictMode react when leading is set to true and trailing is true
3 |
4 | ## 10.0.3
5 | - Removed `peerDependency` part from `package.json` as NPM cannot correctly resolve `peerDependency` for beta and rc versions: see https://stackoverflow.com/questions/67934358/npm-including-all-range-of-pre-release-when-defining-peer-dependency for context
6 |
7 | ## 10.0.2
8 |
9 | - Fixed: `isPending` does not reset the state if the tracked value hasn't changed.. See https://github.com/xnimorz/use-debounce/issues/178
10 |
11 | ## 10.0.1
12 |
13 | - Fixed flush method return args, thanks to [@h](https://github.com/h)
14 |
15 | ## 10.0.0
16 |
17 | - _Major breaking change_: replaced `index.modern.js` with `index.mjs`. Might require a little change in your build pipeline
18 | - _Major breaking change_: New option `debounceOnServer`: if you put the option to true, it will run debouncing on server (via `setTimeout`). The new option can break your current server behaviour, as v9.x, it runs all the time and might cause unnessesary server CPU utilisation. Now, by default, debounced callbacks do not happen on server.
19 | - _Minor breaking change_: Replaced `useState` for `useDebounce` with `useReducer`. It might lead to reduced amount of re-renders, as useState is known to have excess re-renders in some corner: https://stackoverflow.com/questions/57652176/react-hooks-usestate-setvalue-still-rerender-one-more-time-when-value-is-equal
20 | - _Minor breaking change_: `useDebouncedCallback` now updates function to call asap. Meaning, if you re-called the hook and it should trigger immediately, it will trigger the newest function all the time.
21 | - Lib size:
22 | 914 B: index.js.gz
23 | 851 B: index.js.br
24 | 883 B: index.mjs.gz
25 | 826 B: index.mjs.br
26 | 938 B: index.module.js.gz
27 | 873 B: index.module.js.br
28 | 989 B: index.umd.js.gz
29 | 919 B: index.umd.js.br
30 | - [Internal] Replaced Enzyme with @testing-library
31 | - [Internal] yarn classic => npm
32 | - [Internal] Updated devDependencies
33 |
34 | ## 9.0.4
35 |
36 | - Tweak exports, see [PR](https://github.com/xnimorz/use-debounce/pull/160), thanks to [@Andarist](https://github.com/Andarist)
37 | - Changed types, see [PR](https://github.com/xnimorz/use-debounce/pull/158), thanks to [@wangcch](https://github.com/wangcch)
38 |
39 | ## 9.0.3
40 |
41 | - Represent correct return type from useDebounce(), see [issue](https://github.com/xnimorz/use-debounce/pull/155), thanks to [@appden](https://github.com/appden)
42 |
43 | ## 9.0.2
44 |
45 | - Reverted 9.0.0. We will revisit these changes later
46 |
47 | ## 9.0.0
48 |
49 | - Moved use-debounce to support modules see [issue](https://github.com/xnimorz/use-debounce/issues/147) Thanks to [@matewilk](https://github.com/matewilk)
50 | - _breaking change_ The path to `dist/index.js` is changed. Now it's `dist/index.cjs`.
51 |
52 | ## 8.0.4
53 |
54 | - Changes types for `useDebouncedCallback` args: https://github.com/xnimorz/use-debounce/pull/140 Thanks to [@sarunast](https://github.com/sarunast)
55 |
56 | ## 8.0.3
57 |
58 | - Added `types` to package json to mitigate https://github.com/microsoft/TypeScript/issues/49160. https://github.com/xnimorz/use-debounce/pull/138 Thanks to [@wuzzeb](https://github.com/wuzzeb)
59 |
60 | ## 8.0.2
61 |
62 | - Added type exports. https://github.com/xnimorz/use-debounce/pull/136 Thanks to [@tryggvigy](https://github.com/tryggvigy)
63 | - Improved code comments. https://github.com/xnimorz/use-debounce/pull/135 Thanks to [@tryggvigy](https://github.com/tryggvigy)
64 |
65 | ## 8.0.1
66 |
67 | - update library exports section to make exports work correctly with jest@28
68 |
69 | ## 8.0.0
70 |
71 | - _breaking change_ `useDebounce` changed its build system to microbundle. For now we have several entries:
72 |
73 | `index.js` is for commonJS approach
74 | `index.modern.js` for esnext module system
75 | `index.umd.js` for UMD.
76 | All the files are in `dist` folder.
77 |
78 | If you have any paths which have `esm` or `lib`, please, replace them to `dist`:
79 |
80 | Before:
81 |
82 | ```js
83 | import useDebounceCallback from 'use-debounce/lib/useDebounceCallback';
84 | ```
85 |
86 | After:
87 |
88 | ```js
89 | import { useDebounceCallback } from 'use-debounce';
90 | ```
91 |
92 | - Fixed issue with incorrect VSCode autocomplete https://github.com/xnimorz/use-debounce/issues/131 Thanks to [@c-ehrlich](https://github.com/c-ehrlich) for reporting
93 | - Fixed `useDebounce` behaviour with react-devtools tab when devtools have a component with `useDebounce` or `useDebounceCallback` opened. https://github.com/xnimorz/use-debounce/issues/129 Thanks to [@alexniarchos](https://github.com/alexniarchos) for reporting
94 | - Fixed issue with `leading: true` https://github.com/xnimorz/use-debounce/issues/124 Thanks to [@mntnoe](https://github.com/mntnoe) for reporting
95 |
96 | ## 7.0.1
97 |
98 | - `debounced` object now is preserved for `use-debounce` between the renders. Thanks to [@msharifi99](https://github.com/msharifi99) for reporting.
99 |
100 | ## 7.0.0
101 |
102 | - _breaking change_ `useDebounce` hook changed `isPending` behavior from `async` reacting to the sync. Now `isPending` returns `True` as soon as the new value is sent to the hook.
103 | - Dev dependencies updated
104 |
105 | ## 6.0.1
106 |
107 | - Fixed `useDebouncedCallback` return type. Closed https://github.com/xnimorz/use-debounce/issues/103 thanks to [@VanTanev](https://github.com/VanTanev)
108 |
109 | ## 6.0.0
110 |
111 | - _breaking change_: removed `callback` field, instead of this `useDebouncedCallback` and `useThrottledCallback` returns a callable function:
112 | Old:
113 |
114 | ```js
115 | const { callback, pending } = useDebouncedCallback(/*...*/);
116 | // ...
117 | debounced.callback();
118 | ```
119 |
120 | New:
121 |
122 | ```js
123 | const debounced = useDebouncedCallback(/*...*/);
124 | // ...
125 | debounced();
126 | /**
127 | * Also debounced has fields:
128 | * {
129 | * cancel: () => void
130 | * flush: () => void
131 | * isPending: () => boolean
132 | * }
133 | * So you can call debounced.cancel(), debounced.flush(), debounced.isPending()
134 | */
135 | ```
136 |
137 | It makes easier to understand which cancel \ flush or isPending is called in case you have several debounced functions in your component
138 |
139 | - _breaking change_: Now `useDebounce`, `useDebouncedCallback` and `useThrottledCallback` has `isPending` method instead of `pending`
140 |
141 | Old:
142 |
143 | ```js
144 | const { callback, pending } = useDebouncedCallback(/*...*/);
145 | ```
146 |
147 | New:
148 |
149 | ```js
150 | const { isPending } = useDebouncedCallback(/*...*/);
151 | /**
152 | * {
153 | * cancel: () => void
154 | * flush: () => void
155 | * isPending: () => boolean
156 | * }
157 | */
158 | ```
159 |
160 | - get rid of `useCallback` calls
161 |
162 | - improve internal typing
163 |
164 | - decrease the amount of functions to initialize each `useDebouncedCallback` call
165 |
166 | - reduce library size:
167 |
168 | Whole library: from 946 B to 899 B === 47 B
169 | useDebounce: from 844 to 791 === 53 B
170 | useDebouncedCallback: from 680 to 623 === 57 B
171 | useThrottledCallback: from 736 to 680 === 56 B
172 |
173 | ## 5.2.1
174 |
175 | - prevent having ininite setTimeout setup when component gets unmounted https://github.com/xnimorz/use-debounce/issues/97
176 | - function type works correctly with `useDebounce` now. https://github.com/xnimorz/use-debounce/pull/95 Thanks to [@csu-feizao](https://github.com/csu-feizao)
177 |
178 | ## 5.2.0
179 |
180 | - Added `useThrottledCallback`
181 |
182 | ## 5.1.0
183 |
184 | — `wait` param is optional. If you don't provide a wait argument, use-debounce will postpone a callback with requestAnimationFrame if it's in browser environment, or through setTimeout(..., 0) otherwise.
185 |
186 | ## 5.0.4
187 |
188 | - Add an export for React Native
189 |
190 | ## 5.0.3
191 |
192 | - Fix the export map (https://github.com/xnimorz/use-debounce/issues/84);
193 |
194 | ## 5.0.2
195 |
196 | - Add size-limit and configure it for esm modules. Now the size of the whole library is limited within 1 KB (thanks to [@omgovich](https://github.com/omgovich))
197 |
198 | - Add an [export map](https://docs.skypack.dev/package-authors/package-checks#export-map) to your package.json. (thanks to [@omgovich](https://github.com/omgovich))
199 |
200 | - Reduce bundle size (thanks to [@omgovich](https://github.com/omgovich)):
201 | Before:
202 |
203 | ```
204 | esm/index.js
205 | Size: 908 B with all dependencies, minified and gzipped
206 |
207 | esm/index.js
208 | Size: 873 B with all dependencies, minified and gzipped
209 |
210 | esm/index.js
211 | Size: 755 B with all dependencies, minified and gzipped
212 | ```
213 |
214 | Now:
215 |
216 | ```
217 | esm/index.js
218 | Size: 826 B with all dependencies, minified and gzipped
219 |
220 | esm/index.js
221 | Size: 790 B with all dependencies, minified and gzipped
222 |
223 | esm/index.js
224 | Size: 675 B with all dependencies, minified and gzipped
225 | ```
226 |
227 | - Add notes about returned value from `debounced.callback` and its subsequent calls: https://github.com/xnimorz/use-debounce#returned-value-from-debouncedcallback
228 |
229 | - Add project logo (thanks to [@omgovich](https://github.com/omgovich)):
230 |
231 |
232 | ## 5.0.1
233 |
234 | - Fix typing to infer correct callback type (thanks to [@lytc](https://github.com/lytc))
235 |
236 | ## 5.0.0
237 |
238 | - _breaking change_: Now `useDebouncedCallback` returns an object instead of array:
239 |
240 | Old:
241 |
242 | ```js
243 | const [debouncedCallback, cancelDebouncedCallback, callPending] =
244 | useDebouncedCallback(/*...*/);
245 | ```
246 |
247 | New:
248 |
249 | ```js
250 | const debounced = useDebouncedCallback(/*...*/);
251 | /**
252 | * debounced: {
253 | * callback: (...args: T) => unknown, which is debouncedCallback
254 | * cancel: () => void, which is cancelDebouncedCallback
255 | * flush: () => void, which is callPending
256 | * pending: () => boolean, which is a new function
257 | * }
258 | */
259 | ```
260 |
261 | - _breaking change_: Now `useDebounce` returns an array of 2 fields instead of a plain array:
262 | Old:
263 |
264 | ```js
265 | const [value, cancel, callPending] = useDebounce(/*...*/);
266 | ```
267 |
268 | New:
269 |
270 | ```js
271 | const [value, fn] = useDebounce(/*...*/);
272 | /**
273 | * value is just a value without changes
274 | * But fn now is an object: {
275 | * cancel: () => void, which is cancel
276 | * flush: () => void, which is callPending
277 | * pending: () => boolean, which is a new function
278 | * }
279 | */
280 | ```
281 |
282 | - Added `pending` function to both `useDebounce` and `useDebouncedCallback` which shows whether component has pending callbacks
283 | Example:
284 |
285 | ```js
286 | function Component({ text }) {
287 | const debounced = useDebouncedCallback(
288 | useCallback(() => {}, []),
289 | 500
290 | );
291 |
292 | expect(debounced.pending()).toBeFalsy();
293 | debounced.callback();
294 | expect(debounced.pending()).toBeTruthy();
295 | debounced.flush();
296 | expect(debounced.pending()).toBeFalsy();
297 |
298 | return {text} ;
299 | }
300 | ```
301 |
302 | For more details of these major changes you could check this commit https://github.com/xnimorz/use-debounce/commit/1b4ac0432f7074248faafcfe6248df0be4bb4af0 and this issue https://github.com/xnimorz/use-debounce/issues/61
303 |
304 | - Fixed security alerts
305 |
306 | ## 4.0.0
307 |
308 | - _breaking change_: Support lodash style throttling options for trailing+maxWidth. Thanks to [@tryggvigy](https://github.com/tryggvigy)
309 | Example:
310 |
311 | ```js
312 | useDebouncedCallback(callback, 300, {
313 | leading: true,
314 | trailing: false,
315 | maxWait: 300,
316 | });
317 | ```
318 |
319 | Where the trailing edge is turned off. Let's say the function is called twice in the first 300ms. Now debounced function to have been called _once_.
320 |
321 | _how to migrate_: Please, check your `traling: false` params with `maxWait` option
322 |
323 | - _breaking change_: Now in case delay option is unset, it will be `requestAnimationFrame` delay
324 |
325 | - _breaking change_: Now `debouncedCallback` from `useDebouncedCallback` returns a value. In v3 it used to return `undefined`:
326 |
327 | ## 3.4.3
328 |
329 | - Fix use-debounce so that it works correctly with react-native and next.js (as both of them use fast-refresh).
330 |
331 | ## 3.4.2
332 |
333 | - Clear cache in build directory. Thanks to [@wangcch](https://github.com/wangcch)
334 |
335 | ## 3.4.1
336 |
337 | - update types, so that they are more convinient. Thanks to [@astj](https://github.com/astj)
338 |
339 | ## 3.4.0
340 |
341 | - Now `callPendings` wrapped with useCallback hook, so that the reference to the function would be the same. Thanks to [@jfschwarz](https://github.com/jfschwarz)
342 |
343 | ## 3.3.0
344 |
345 | - `useDebouncedCallback` and `useDebounce` now can configure both `leading` and `trailing` options. They are fully compatible with `lodash.debounce` function https://lodash.com/docs/4.17.15#debounce. `leading` by default is false, trailing by default is true.
346 | Examples: https://codesandbox.io/s/vigilant-bush-zrbzg
347 | https://github.com/xnimorz/use-debounce/blob/master/test/useDebouncedCallback.test.tsx#L29-L180
348 |
349 | ## 3.2.0
350 |
351 | - `useDebounce` has `callPending` method. See https://github.com/xnimorz/use-debounce/blob/master/test/useDebounce.test.tsx#L276-L302 unit test for examples.
352 |
353 | ## 3.1.0
354 |
355 | - Now package includes only nessesary files. Thanks to [@vkrol](https://github.com/vkrol)
356 | - Added optional `equalityFn` to `options` object for `useDebounce` so that you can provide a custom equality function to the hook. Thanks to [@seruco](https://github.com/seruco)
357 |
358 | ## 3.0.1
359 |
360 | - Added missed `esm` directory (thanks for reporting [@FredyC](https://github.com/FredyC))
361 | - Fixed import name (thanks for PR [@neoromantic](https://github.com/neoromantic))
362 | - Updated `eslint-utils` lib version due to security issue
363 |
364 | ## 3.0.0
365 |
366 | - **breaking change** now, `cache` file renamed to `useDebounce` and `callback` file renamed to `useDebouncedCallback`.
367 | If you used to import file by its path:
368 |
369 | ```js
370 | import useDebounce from 'use-debounce/lib/cache';
371 | import useDebouncedCallback from 'use-debounce/lib/callback';
372 | ```
373 |
374 | it should be renamed to
375 |
376 | ```js
377 | import useDebounce from 'use-debounce/lib/useDebounce';
378 | import useDebouncedCallback from 'use-debounce/lib/useDebouncedCallback';
379 | ```
380 |
381 | It helps us to keep more descriptive names. Thanks to [@vkrol](https://github.com/vkrol)
382 | https://github.com/xnimorz/use-debounce/pull/33
383 |
384 | - **breaking change** now, `useDebouncedCallback` executes the latest callback, which was sent to the hook (thanks for the report [@alexandr-bbm](https://github.com/alexandr-bbm) https://github.com/xnimorz/use-debounce/issues/35)
385 | https://github.com/xnimorz/use-debounce/commit/eca14cc25b1f14bdd337a555127fd98c54ab7a5c
386 |
387 | - code shipped in ESM format. Thanks to [@vkrol](https://github.com/vkrol)
388 | https://github.com/xnimorz/use-debounce/pull/34
389 |
390 | ## 2.2.1
391 |
392 | — Added `types` field in package.json. Thanks to [@nmussy](https://github.com/nmussy)
393 |
394 | ## 2.2.0
395 |
396 | - Added leading calls param https://github.com/xnimorz/use-debounce#leading-calls thanks to [@Pringels](https://github.com/Pringels)
397 | - Updated dev-dependencies
398 |
399 | ## 2.1.0
400 |
401 | - Rewrite to typescript
402 |
403 | ## 2.0.1
404 |
405 | - Fix the issue https://github.com/xnimorz/use-debounce/issues/23. Thanks to [@anilanar](https://github.com/anilanar) for reporting it.
406 | - Add eslint to the project
407 |
408 | ## 2.0.0
409 |
410 | - **breaking changes** now, `useDebouncedCallback` doesn't have `deps` argument. If you want to cache your callback it's better to use:
411 |
412 | ```js
413 | const myCallback = useDebouncedCallback(
414 | useCallback(() => {
415 | /* do some stuff */
416 | }, [value]),
417 | 500
418 | );
419 | ```
420 |
421 | - added `size-limit` to the project.
422 | - Reduce size of the library from 705 bytes to 352 bytes (50%)
423 |
424 | ## 1.1.3
425 |
426 | - remove `react-dom` from peerDependencies (as you can use this library with react native).
427 |
428 | ## 1.1.2
429 |
430 | - `useCallback` now memoize returned callback
431 |
432 | ## 1.1.0
433 |
434 | - add `callPending` callback to `useDebouncedCallback` method. It allows to call the callback manually if it hasn't fired yet. This method is handy to use when the user takes an action that would cause the component to unmount, but you need to execute the callback.
435 |
436 | ```javascript
437 | import React, { useState, useCallback } from 'react';
438 | import useDebouncedCallback from 'use-debounce/lib/callback';
439 |
440 | function InputWhichFetchesSomeData({ defaultValue, asyncFetchData }) {
441 | const [debouncedFunction, cancel, callPending] = useDebouncedCallback(
442 | (value) => {
443 | asyncFetchData;
444 | },
445 | 500,
446 | [],
447 | { maxWait: 2000 }
448 | );
449 |
450 | // When the component goes to be unmounted, we will fetch data if the input has changed.
451 | useEffect(
452 | () => () => {
453 | callPending();
454 | },
455 | []
456 | );
457 |
458 | return (
459 | debouncedFunction(e.target.value)}
462 | />
463 | );
464 | }
465 | ```
466 |
467 | More examples are available here: https://github.com/xnimorz/use-debounce/commit/989d6c0efb4eef080ed78330233186d7b0c249e3#diff-c7e0cfdec8acc174d3301ff43b986264R196
468 |
469 | ## 1.0.0
470 |
471 | The example with all features you can see here: https://codesandbox.io/s/4wvmp1xlw4
472 |
473 | - add maxWait option. The maximum time func is allowed to be delayed before it's invoked:
474 |
475 | ```javascript
476 | import { useDebounce, useDebouncedCallback } from 'use-debounce';
477 |
478 | ...
479 | const debouncedValue = useDebounce(value, 300, {maxWait: 1000});
480 | const debouncedCallback = useDebouncedCallback(() => {...}, 300, [], {maxWait: 1000});
481 | ```
482 |
483 | - add cancel callback (thanks to [@thibaultboursier](https://github.com/thibaultboursier) for contributing). Cancel callback removes func from the queue (even maxWait):
484 |
485 | ```javascript
486 | import { useDebounce, useDebouncedCallback } from 'use-debounce';
487 |
488 | ...
489 | const [ debouncedValue, cancelValueDebouncingCycle ] = useDebounce(value, 1000);
490 | const [ debouncedCallback, cancelCallback ] = useDebouncedCallback(() => {...}, 1000);
491 | ```
492 |
493 | - [BREAKING] change the contact of use-debounce callback and value hooks:
494 |
495 | **Old:**
496 |
497 | ```javascript
498 | import { useDebounce, useDebouncedCallback } from 'use-debounce';
499 |
500 | ...
501 | const debouncedValue = useDebounce(value, 1000);
502 | const debouncedCallback = useDebouncedCallback(() => {...}, 1000);
503 | ```
504 |
505 | **New:**
506 |
507 | ```javascript
508 | import { useDebounce, useDebouncedCallback } from 'use-debounce';
509 |
510 | ...
511 | const [ debouncedValue, cancelValueDebouncingCycle ] = useDebounce(value, 1000);
512 | const [ debouncedCallback, cancelCallback ] = useDebouncedCallback(() => {...}, 1000);
513 | ```
514 |
515 | You still can use only value and callback:
516 |
517 | ```javascript
518 | import { useDebounce, useDebouncedCallback } from 'use-debounce';
519 |
520 | ...
521 | const [ debouncedValue ] = useDebounce(value, 1000);
522 | const [ debouncedCallback ] = useDebouncedCallback(() => {...}, 1000);
523 | ```
524 |
525 | ## 0.0.x
526 |
527 | - add use-debounce callback and use-debounce value. First one is useful for debouncing callbacks e.g. event handlers, second one is handy for debouncing a value such as search fields etc.
528 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Nikita Mostovoy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
6 |
7 |
21 |
22 | # useDebounce, useDebouncedCallback & useThrottledCallback
23 |
24 | React libraries for debouncing without tears!
25 |
26 | - Small size < 1 Kb
27 | - Compatible with underscore / lodash impl — learn once, use everywhere
28 | - Server-rendering friendly!
29 |
30 | ## Features
31 |
32 | - [classic debounced callback](#debounced-callbacks)
33 | - [**value** debouncing](#simple-values-debouncing)
34 | - [cancel, maxWait and memoization](#advanced-usage)
35 |
36 | ## Install
37 |
38 | ```sh
39 | yarn add use-debounce
40 | # or
41 | npm i use-debounce --save
42 | ```
43 |
44 | ## Copy paste guidance:
45 |
46 | ### use-debounce
47 |
48 | Simple usage: https://codesandbox.io/s/kx75xzyrq7
49 |
50 | Debounce HTTP request: https://codesandbox.io/s/rr40wnropq
51 |
52 | Debounce HTTP request with `leading` param: https://codesandbox.io/s/cache-example-with-areas-and-leading-param-119r3i
53 |
54 | ### use-debounce callback
55 |
56 | Simple usage: https://codesandbox.io/s/x0jvqrwyq
57 |
58 | Combining with native event listeners: https://codesandbox.io/s/32yqlyo815
59 |
60 | Cancelling, maxWait and memoization: https://codesandbox.io/s/4wvmp1xlw4
61 |
62 | HTTP requests: https://codesandbox.io/s/use-debounce-callback-http-y1h3m6
63 |
64 | ## Changelog
65 |
66 | https://github.com/xnimorz/use-debounce/blob/master/CHANGELOG.md
67 |
68 | ## Simple values debouncing
69 |
70 | According to https://twitter.com/dan_abramov/status/1060729512227467264
71 | WebArchive link: https://web.archive.org/web/20210828073432/https://twitter.com/dan_abramov/status/1060729512227467264
72 |
73 | ```javascript
74 | import React, { useState } from 'react';
75 | import { useDebounce } from 'use-debounce';
76 |
77 | export default function Input() {
78 | const [text, setText] = useState('Hello');
79 | const [value] = useDebounce(text, 1000);
80 |
81 | return (
82 |
83 |
{
86 | setText(e.target.value);
87 | }}
88 | />
89 |
Actual value: {text}
90 |
Debounce value: {value}
91 |
92 | );
93 | }
94 | ```
95 |
96 | This hook compares prev and next value using shallow equal. It means, setting an object `{}` will trigger debounce timer. If you have to compare objects (https://github.com/xnimorz/use-debounce/issues/27#issuecomment-496828063), you can use `useDebouncedCallback`, that is explained below:
97 |
98 | ## Debounced callbacks
99 |
100 | Besides `useDebounce` for values you can debounce callbacks, that is the more commonly understood kind of debouncing.
101 | Example with Input (and react callbacks): https://codesandbox.io/s/x0jvqrwyq
102 |
103 | ```js
104 | import { useDebouncedCallback } from 'use-debounce';
105 |
106 | function Input({ defaultValue }) {
107 | const [value, setValue] = useState(defaultValue);
108 | // Debounce callback
109 | const debounced = useDebouncedCallback(
110 | // function
111 | (value) => {
112 | setValue(value);
113 | },
114 | // delay in ms
115 | 1000
116 | );
117 |
118 | // you should use `e => debounced(e.target.value)` as react works with synthetic events
119 | return (
120 |
121 |
debounced(e.target.value)}
124 | />
125 |
Debounced value: {value}
126 |
127 | );
128 | }
129 | ```
130 |
131 | Example with Scroll (and native event listeners): https://codesandbox.io/s/32yqlyo815
132 |
133 | ```js
134 | function ScrolledComponent() {
135 | // just a counter to show, that there are no any unnessesary updates
136 | const updatedCount = useRef(0);
137 | updatedCount.current++;
138 |
139 | const [position, setPosition] = useState(window.pageYOffset);
140 |
141 | // Debounce callback
142 | const debounced = useDebouncedCallback(
143 | // function
144 | () => {
145 | setPosition(window.pageYOffset);
146 | },
147 | // delay in ms
148 | 800
149 | );
150 |
151 | useEffect(() => {
152 | const unsubscribe = subscribe(window, 'scroll', debounced);
153 | return () => {
154 | unsubscribe();
155 | };
156 | }, []);
157 |
158 | return (
159 |
160 |
161 |
Debounced top position: {position}
162 |
Component rerendered {updatedCount.current} times
163 |
164 |
165 | );
166 | }
167 | ```
168 |
169 | ### Returned value from `debounced()`
170 |
171 | Subsequent calls to the debounced function `debounced` return the result of the last func invocation.
172 | Note, that if there are no previous invocations it's mean you will get undefined. You should check it in your code properly.
173 |
174 | Example:
175 |
176 | ```javascript
177 | it('Subsequent calls to the debounced function `debounced` return the result of the last func invocation.', () => {
178 | const callback = jest.fn(() => 42);
179 |
180 | let callbackCache;
181 | function Component() {
182 | const debounced = useDebouncedCallback(callback, 1000);
183 | callbackCache = debounced;
184 | return null;
185 | }
186 | Enzyme.mount( );
187 |
188 | const result = callbackCache();
189 | expect(callback.mock.calls.length).toBe(0);
190 | expect(result).toBeUndefined();
191 |
192 | act(() => {
193 | jest.runAllTimers();
194 | });
195 | expect(callback.mock.calls.length).toBe(1);
196 | const subsequentResult = callbackCache();
197 |
198 | expect(callback.mock.calls.length).toBe(1);
199 | expect(subsequentResult).toBe(42);
200 | });
201 | ```
202 |
203 | ### Advanced usage
204 |
205 | #### Cancel, maxWait and memoization
206 |
207 | 1. Both `useDebounce` and `useDebouncedCallback` works with `maxWait` option. This params describes the maximum time func is allowed to be delayed before it's invoked.
208 | 2. You can cancel debounce cycle, by calling `cancel` callback
209 |
210 | The full example you can see here https://codesandbox.io/s/4wvmp1xlw4
211 |
212 | ```javascript
213 | import React, { useState } from 'react';
214 | import ReactDOM from 'react-dom';
215 | import { useDebouncedCallback } from 'use-debounce';
216 |
217 | function Input({ defaultValue }) {
218 | const [value, setValue] = useState(defaultValue);
219 | const debounced = useDebouncedCallback(
220 | (value) => {
221 | setValue(value);
222 | },
223 | 500,
224 | // The maximum time func is allowed to be delayed before it's invoked:
225 | { maxWait: 2000 }
226 | );
227 |
228 | // you should use `e => debounced(e.target.value)` as react works with synthetic events
229 | return (
230 |
231 |
debounced(e.target.value)}
234 | />
235 |
Debounced value: {value}
236 |
Cancel Debounce cycle
237 |
238 | );
239 | }
240 |
241 | const rootElement = document.getElementById('root');
242 | ReactDOM.render( , rootElement);
243 | ```
244 |
245 | The same API is available for `useDebounce` calls:
246 | ```js
247 | const [value, {cancel, isPending, flush}] = useDebounce(valueToDebounce);
248 | ...
249 | cancel() // cancels pending debounce request
250 | isPending() // returns if there is a pending debouncing request
251 | flush() // immediately flushes pending request
252 | ```
253 |
254 |
255 | #### Flush method
256 |
257 | `useDebouncedCallback` has `flush` method. It allows to call the callback manually if it hasn't fired yet. This method is handy to use when the user takes an action that would cause the component to unmount, but you need to execute the callback.
258 |
259 | ```javascript
260 | import React, { useState } from 'react';
261 | import { useDebouncedCallback } from 'use-debounce';
262 |
263 | function InputWhichFetchesSomeData({ defaultValue, asyncFetchData }) {
264 | const debounced = useDebouncedCallback(
265 | (value) => {
266 | asyncFetchData;
267 | },
268 | 500,
269 | { maxWait: 2000 }
270 | );
271 |
272 | // When the component goes to be unmounted, we will fetch data if the input has changed.
273 | useEffect(
274 | () => () => {
275 | debounced.flush();
276 | },
277 | [debounced]
278 | );
279 |
280 | return (
281 | debounced(e.target.value)}
284 | />
285 | );
286 | }
287 | ```
288 |
289 | #### isPending method
290 |
291 | `isPending` method shows whether component has pending callbacks. Works for both `useDebounce` and `useDebouncedCallback`:
292 |
293 | ```javascript
294 | import React, { useCallback } from 'react';
295 |
296 | function Component({ text }) {
297 | const debounced = useDebouncedCallback(
298 | useCallback(() => {}, []),
299 | 500
300 | );
301 |
302 | expect(debounced.isPending()).toBeFalsy();
303 | debounced();
304 | expect(debounced.isPending()).toBeTruthy();
305 | debounced.flush();
306 | expect(debounced.isPending()).toBeFalsy();
307 |
308 | return {text} ;
309 | }
310 | ```
311 |
312 | #### leading/trailing calls
313 |
314 | Both `useDebounce` and `useDebouncedCallback` work with the `leading` and `trailing` options. `leading` param will execute the function once immediately when called. Subsequent calls will be debounced until the timeout expires. `trailing` option controls whenever to call the callback after timeout again.
315 |
316 | For more information on how leading debounce calls work see: https://lodash.com/docs/#debounce
317 |
318 | ```javascript
319 | import React, { useState } from 'react';
320 | import { useDebounce } from 'use-debounce';
321 |
322 | export default function Input() {
323 | const [text, setText] = useState('Hello');
324 | const [value] = useDebounce(text, 1000, { leading: true });
325 |
326 | // value is updated immediately when text changes the first time,
327 | // but all subsequent changes are debounced.
328 | return (
329 |
330 |
{
333 | setText(e.target.value);
334 | }}
335 | />
336 |
Actual value: {text}
337 |
Debounce value: {value}
338 |
339 | );
340 | }
341 | ```
342 |
343 | #### Options:
344 |
345 | You can provide additional options as a third argument to both `useDebounce` and `useDebouncedCallback`:
346 |
347 | | option | default | Description | Example |
348 | | ---------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
349 | | maxWait | - | Describes the maximum time func is allowed to be delayed before it's invoked | https://github.com/xnimorz/use-debounce#cancel-maxwait-and-memoization |
350 | | leading | - | This param will execute the function once immediately when called. Subsequent calls will be debounced until the timeout expires. | https://github.com/xnimorz/use-debounce#leading-calls |
351 | | trailing | true | This param executes the function after timeout. | https://github.com/xnimorz/use-debounce#leading-calls |
352 | | equalityFn | (prev, next) => prev === next | [useDebounce ONLY] Comparator function which shows if timeout should be started | |
353 |
354 | ## useThrottledCallback
355 |
356 | You are able to use throttled callback with this library also (starting 5.2.0 version).
357 | For this purpose use:
358 |
359 | ```
360 | import useThrottledCallback from 'use-debounce/useThrottledCallback';
361 | ```
362 |
363 | or
364 |
365 | ```
366 | import { useThrottledCallback } from 'use-debounce';
367 | ```
368 |
369 | Several examples:
370 |
371 | 1. Avoid excessively updating the position while scrolling.
372 |
373 | ```js
374 | const scrollHandler = useThrottledCallback(updatePosition, 100);
375 | window.addEventListener('scroll', scrollHandler);
376 | ```
377 |
378 | 2. Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes.
379 | ```js
380 | const throttled = useThrottledCallback(renewToken, 300000, { 'trailing': false })
381 | click
382 | ```
383 |
384 | All the params for `useThrottledCallback` are the same as for `useDebouncedCallback` except `maxWait` option. As it's not needed for throttle callbacks.
385 |
386 | # Special thanks:
387 |
388 | [@tryggvigy](https://github.com/tryggvigy) — for managing lots of new features of the library like trailing and leading params, throttle callback, etc;
389 |
390 | [@omgovich](https://github.com/omgovich) — for reducing bundle size.
391 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('ts-jest').JestConfigWithTsJest} */
2 | module.exports = {
3 | preset: 'ts-jest',
4 | testEnvironment: 'jsdom',
5 | roots: ['/test'],
6 | setupFilesAfterEnv: ["/setupTests.ts"],
7 | };
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xnimorz/use-debounce/c10b7e8b9653aab00e20a12aac2fe6f4c068dfee/logo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "use-debounce",
3 | "version": "10.0.4",
4 | "description": "Debounce hook for react",
5 | "source": "src/index.ts",
6 | "main": "dist/index.js",
7 | "module": "dist/index.module.js",
8 | "esmodule": "dist/index.mjs",
9 | "umd:main": "dist/index.umd.js",
10 | "exports": {
11 | ".": {
12 | "types": "./dist/index.d.ts",
13 | "module": "./dist/index.module.js",
14 | "import": "./dist/index.mjs",
15 | "require": "./dist/index.js"
16 | },
17 | "./package.json": "./package.json"
18 | },
19 | "sideEffects": false,
20 | "scripts": {
21 | "jest": "jest",
22 | "size": "npm run build-only && size-limit",
23 | "test": "jest && eslint \"src/**.ts\"",
24 | "build-only": "rm -rf ./dist/*; microbundle build --entry src/index.ts --name use-debounce --tsconfig tsconfig.json",
25 | "build": "npm run test && npm run build-only && size-limit",
26 | "prepublishOnly": "npm run build"
27 | },
28 | "engines": {
29 | "node": ">= 16.0.0"
30 | },
31 | "repository": {
32 | "type": "git",
33 | "url": "git+ssh://git@github.com/xnimorz/use-debounce.git"
34 | },
35 | "keywords": [
36 | "debounce",
37 | "react-hook",
38 | "react"
39 | ],
40 | "author": "Nik (nik@xnim.me)",
41 | "license": "MIT",
42 | "bugs": {
43 | "url": "https://github.com/xnimorz/use-debounce/issues"
44 | },
45 | "files": [
46 | "dist"
47 | ],
48 | "peerDependencies": {
49 | "react": "*"
50 | },
51 | "homepage": "https://github.com/xnimorz/use-debounce#readme",
52 | "devDependencies": {
53 | "@size-limit/preset-small-lib": "^10.0.1",
54 | "@testing-library/jest-dom": "^6.1.4",
55 | "@testing-library/react": "^14.0.0",
56 | "@testing-library/user-event": "^14.5.1",
57 | "@types/jest": "^29.5.7",
58 | "@types/node": "^20.8.9",
59 | "@types/react": "^18.2.33",
60 | "@types/react-dom": "^18.2.14",
61 | "@typescript-eslint/eslint-plugin": "^6.9.1",
62 | "@typescript-eslint/parser": "^6.9.1",
63 | "eslint": "^8.52.0",
64 | "eslint-config-prettier": "^9.0.0",
65 | "eslint-config-standard": "^17.1.0",
66 | "eslint-plugin-import": "^2.29.0",
67 | "eslint-plugin-node": "^11.1.0",
68 | "eslint-plugin-prettier": "^5.0.1",
69 | "eslint-plugin-promise": "^6.1.1",
70 | "eslint-plugin-react-hooks": "^4.6.0",
71 | "eslint-plugin-standard": "^4.1.0",
72 | "jest": "^29.7.0",
73 | "jest-environment-jsdom": "^29.7.0",
74 | "microbundle": "^0.15.1",
75 | "prettier": "^3.0.3",
76 | "react": "18.2.0",
77 | "react-dom": "18.2.0",
78 | "size-limit": "^10.0.1",
79 | "ts-jest": "^29.1.1",
80 | "typescript": "^5.2.2"
81 | },
82 | "resolutions": {
83 | "kind-of": "6.0.3"
84 | },
85 | "size-limit": [
86 | {
87 | "path": "dist/index.js",
88 | "limit": "1.2 KB"
89 | },
90 | {
91 | "path": "dist/index.module.js",
92 | "limit": "1.2 KB"
93 | },
94 | {
95 | "path": "dist/index.umd.js",
96 | "limit": "1.2 KB"
97 | },
98 | {
99 | "path": "dist/index.js",
100 | "import": "{ useDebounce }",
101 | "limit": "1.2 KB"
102 | },
103 | {
104 | "path": "dist/index.js",
105 | "import": "{ useDebouncedCallback }",
106 | "limit": "1.2 KB"
107 | },
108 | {
109 | "path": "dist/index.js",
110 | "import": "{ useThrottledCallback }",
111 | "limit": "1.2 KB"
112 | }
113 | ]
114 | }
115 |
--------------------------------------------------------------------------------
/setupTests.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import useDebounce from './useDebounce';
2 | import useDebouncedCallback from './useDebouncedCallback';
3 | import type {
4 | CallOptions,
5 | ControlFunctions,
6 | DebouncedState,
7 | Options,
8 | } from './useDebouncedCallback';
9 | import useThrottledCallback from './useThrottledCallback';
10 |
11 | export { useDebounce, useDebouncedCallback, useThrottledCallback };
12 |
13 | export { CallOptions, ControlFunctions, DebouncedState, Options };
14 |
--------------------------------------------------------------------------------
/src/useDebounce.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useRef, useState } from 'react';
2 | import useDebouncedCallback, { DebouncedState } from './useDebouncedCallback';
3 |
4 | function valueEquality(left: T, right: T): boolean {
5 | return left === right;
6 | }
7 |
8 | export default function useDebounce(
9 | value: T,
10 | delay: number,
11 | options?: {
12 | maxWait?: number;
13 | leading?: boolean;
14 | trailing?: boolean;
15 | equalityFn?: (left: T, right: T) => boolean;
16 | }
17 | ): [T, DebouncedState<(value: T) => void>] {
18 | const eq = (options && options.equalityFn) || valueEquality;
19 |
20 | const activeValue = useRef(value);
21 | const [, forceUpdate] = useState({});
22 | const debounced = useDebouncedCallback(
23 | useCallback(
24 | (value: T) => {
25 | activeValue.current = value;
26 | forceUpdate({});
27 | },
28 | [forceUpdate]
29 | ),
30 | delay,
31 | options
32 | );
33 | const previousValue = useRef(value);
34 |
35 | if (!eq(previousValue.current, value)) {
36 | debounced(value);
37 | previousValue.current = value;
38 | }
39 |
40 | return [activeValue.current as T, debounced];
41 | }
42 |
--------------------------------------------------------------------------------
/src/useDebouncedCallback.ts:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect, useMemo } from 'react';
2 |
3 | export interface CallOptions {
4 | /**
5 | * Controls if the function should be invoked on the leading edge of the timeout.
6 | */
7 | leading?: boolean;
8 | /**
9 | * Controls if the function should be invoked on the trailing edge of the timeout.
10 | */
11 | trailing?: boolean;
12 | }
13 |
14 | export interface Options extends CallOptions {
15 | /**
16 | * The maximum time the given function is allowed to be delayed before it's invoked.
17 | */
18 | maxWait?: number;
19 | /**
20 | * If the setting is set to true, all debouncing and timers will happen on the server side as well
21 | */
22 | debounceOnServer?: boolean;
23 | }
24 |
25 | export interface ControlFunctions {
26 | /**
27 | * Cancel pending function invocations
28 | */
29 | cancel: () => void;
30 | /**
31 | * Immediately invoke pending function invocations
32 | */
33 | flush: () => ReturnT | undefined;
34 | /**
35 | * Returns `true` if there are any pending function invocations
36 | */
37 | isPending: () => boolean;
38 | }
39 |
40 | /**
41 | * Subsequent calls to the debounced function return the result of the last func invocation.
42 | * Note, that if there are no previous invocations you will get undefined. You should check it in your code properly.
43 | */
44 | export interface DebouncedState ReturnType>
45 | extends ControlFunctions> {
46 | (...args: Parameters): ReturnType | undefined;
47 | }
48 |
49 | /**
50 | * Creates a debounced function that delays invoking `func` until after `wait`
51 | * milliseconds have elapsed since the last time the debounced function was
52 | * invoked, or until the next browser frame is drawn.
53 | *
54 | * The debounced function comes with a `cancel` method to cancel delayed `func`
55 | * invocations and a `flush` method to immediately invoke them.
56 | *
57 | * Provide `options` to indicate whether `func` should be invoked on the leading
58 | * and/or trailing edge of the `wait` timeout. The `func` is invoked with the
59 | * last arguments provided to the debounced function.
60 | *
61 | * Subsequent calls to the debounced function return the result of the last
62 | * `func` invocation.
63 | *
64 | * **Note:** If `leading` and `trailing` options are `true`, `func` is
65 | * invoked on the trailing edge of the timeout only if the debounced function
66 | * is invoked more than once during the `wait` timeout.
67 | *
68 | * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
69 | * until the next tick, similar to `setTimeout` with a timeout of `0`.
70 | *
71 | * If `wait` is omitted in an environment with `requestAnimationFrame`, `func`
72 | * invocation will be deferred until the next frame is drawn (typically about
73 | * 16ms).
74 | *
75 | * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
76 | * for details over the differences between `debounce` and `throttle`.
77 | *
78 | * @category Function
79 | * @param {Function} func The function to debounce.
80 | * @param {number} [wait=0]
81 | * The number of milliseconds to delay; if omitted, `requestAnimationFrame` is
82 | * used (if available, otherwise it will be setTimeout(...,0)).
83 | * @param {Object} [options={}] The options object.
84 | * Controls if `func` should be invoked on the leading edge of the timeout.
85 | * @param {boolean} [options.leading=false]
86 | * The maximum time `func` is allowed to be delayed before it's invoked.
87 | * @param {number} [options.maxWait]
88 | * Controls if `func` should be invoked the trailing edge of the timeout.
89 | * @param {boolean} [options.trailing=true]
90 | * @returns {Function} Returns the new debounced function.
91 | * @example
92 | *
93 | * // Avoid costly calculations while the window size is in flux.
94 | * const resizeHandler = useDebouncedCallback(calculateLayout, 150);
95 | * window.addEventListener('resize', resizeHandler)
96 | *
97 | * // Invoke `sendMail` when clicked, debouncing subsequent calls.
98 | * const clickHandler = useDebouncedCallback(sendMail, 300, {
99 | * leading: true,
100 | * trailing: false,
101 | * })
102 | * click me
103 | *
104 | * // Ensure `batchLog` is invoked once after 1 second of debounced calls.
105 | * const debounced = useDebouncedCallback(batchLog, 250, { 'maxWait': 1000 })
106 | * const source = new EventSource('/stream')
107 | * source.addEventListener('message', debounced)
108 | *
109 | * // Cancel the trailing debounced invocation.
110 | * window.addEventListener('popstate', debounced.cancel)
111 | *
112 | * // Check for pending invocations.
113 | * const status = debounced.isPending() ? "Pending..." : "Ready"
114 | */
115 | export default function useDebouncedCallback<
116 | T extends (...args: any) => ReturnType,
117 | >(func: T, wait?: number, options?: Options): DebouncedState {
118 | const lastCallTime = useRef(null);
119 | const lastInvokeTime = useRef(0);
120 | const timerId = useRef(null);
121 | const lastArgs = useRef([]);
122 | const lastThis = useRef();
123 | const result = useRef>();
124 | const funcRef = useRef(func);
125 | const mounted = useRef(true);
126 | // Always keep the latest version of debounce callback, with no wait time.
127 | funcRef.current = func;
128 |
129 | const isClientSide = typeof window !== 'undefined';
130 | // Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
131 | const useRAF = !wait && wait !== 0 && isClientSide;
132 |
133 | if (typeof func !== 'function') {
134 | throw new TypeError('Expected a function');
135 | }
136 |
137 | wait = +wait || 0;
138 | options = options || {};
139 |
140 | const leading = !!options.leading;
141 | const trailing = 'trailing' in options ? !!options.trailing : true; // `true` by default
142 | const maxing = 'maxWait' in options;
143 | const debounceOnServer =
144 | 'debounceOnServer' in options ? !!options.debounceOnServer : false; // `false` by default
145 | const maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : null;
146 |
147 | useEffect(() => {
148 | mounted.current = true;
149 | return () => {
150 | mounted.current = false;
151 | };
152 | }, []);
153 |
154 | // You may have a question, why we have so many code under the useMemo definition.
155 | //
156 | // This was made as we want to escape from useCallback hell and
157 | // not to initialize a number of functions each time useDebouncedCallback is called.
158 | //
159 | // It means that we have less garbage for our GC calls which improves performance.
160 | // Also, it makes this library smaller.
161 | //
162 | // And the last reason, that the code without lots of useCallback with deps is easier to read.
163 | // You have only one place for that.
164 | const debounced = useMemo(() => {
165 | const invokeFunc = (time: number) => {
166 | const args = lastArgs.current;
167 | const thisArg = lastThis.current;
168 |
169 | lastArgs.current = lastThis.current = null;
170 | lastInvokeTime.current = time;
171 | return (result.current = funcRef.current.apply(thisArg, args));
172 | };
173 |
174 | const startTimer = (pendingFunc: () => void, wait: number) => {
175 | if (useRAF) cancelAnimationFrame(timerId.current);
176 | timerId.current = useRAF
177 | ? requestAnimationFrame(pendingFunc)
178 | : setTimeout(pendingFunc, wait);
179 | };
180 |
181 | const shouldInvoke = (time: number) => {
182 | if (!mounted.current) return false;
183 |
184 | const timeSinceLastCall = time - lastCallTime.current;
185 | const timeSinceLastInvoke = time - lastInvokeTime.current;
186 |
187 | // Either this is the first call, activity has stopped and we're at the
188 | // trailing edge, the system time has gone backwards and we're treating
189 | // it as the trailing edge, or we've hit the `maxWait` limit.
190 | return (
191 | !lastCallTime.current ||
192 | timeSinceLastCall >= wait ||
193 | timeSinceLastCall < 0 ||
194 | (maxing && timeSinceLastInvoke >= maxWait)
195 | );
196 | };
197 |
198 | const trailingEdge = (time: number) => {
199 | timerId.current = null;
200 |
201 | // Only invoke if we have `lastArgs` which means `func` has been
202 | // debounced at least once.
203 | if (trailing && lastArgs.current) {
204 | return invokeFunc(time);
205 | }
206 | lastArgs.current = lastThis.current = null;
207 | return result.current;
208 | };
209 |
210 | const timerExpired = () => {
211 | const time = Date.now();
212 | if (shouldInvoke(time)) {
213 | return trailingEdge(time);
214 | }
215 | // https://github.com/xnimorz/use-debounce/issues/97
216 | if (!mounted.current) {
217 | return;
218 | }
219 | // Remaining wait calculation
220 | const timeSinceLastCall = time - lastCallTime.current;
221 | const timeSinceLastInvoke = time - lastInvokeTime.current;
222 | const timeWaiting = wait - timeSinceLastCall;
223 | const remainingWait = maxing
224 | ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
225 | : timeWaiting;
226 |
227 | // Restart the timer
228 | startTimer(timerExpired, remainingWait);
229 | };
230 |
231 | const func: DebouncedState = (...args: Parameters): ReturnType => {
232 | if (!isClientSide && !debounceOnServer) {
233 | return;
234 | }
235 | const time = Date.now();
236 | const isInvoking = shouldInvoke(time);
237 |
238 | lastArgs.current = args;
239 | lastThis.current = this;
240 | lastCallTime.current = time;
241 |
242 | if (isInvoking) {
243 | if (!timerId.current && mounted.current) {
244 | // Reset any `maxWait` timer.
245 | lastInvokeTime.current = lastCallTime.current;
246 | // Start the timer for the trailing edge.
247 | startTimer(timerExpired, wait);
248 | // Invoke the leading edge.
249 | return leading ? invokeFunc(lastCallTime.current) : result.current;
250 | }
251 | if (maxing) {
252 | // Handle invocations in a tight loop.
253 | startTimer(timerExpired, wait);
254 | return invokeFunc(lastCallTime.current);
255 | }
256 | }
257 | if (!timerId.current) {
258 | startTimer(timerExpired, wait);
259 | }
260 | return result.current;
261 | };
262 |
263 | func.cancel = () => {
264 | if (timerId.current) {
265 | useRAF
266 | ? cancelAnimationFrame(timerId.current)
267 | : clearTimeout(timerId.current);
268 | }
269 | lastInvokeTime.current = 0;
270 | lastArgs.current =
271 | lastCallTime.current =
272 | lastThis.current =
273 | timerId.current =
274 | null;
275 | };
276 |
277 | func.isPending = () => {
278 | return !!timerId.current;
279 | };
280 |
281 | func.flush = () => {
282 | return !timerId.current ? result.current : trailingEdge(Date.now());
283 | };
284 |
285 | return func;
286 | }, [
287 | leading,
288 | maxing,
289 | wait,
290 | maxWait,
291 | trailing,
292 | useRAF,
293 | isClientSide,
294 | debounceOnServer,
295 | ]);
296 |
297 | return debounced;
298 | }
299 |
--------------------------------------------------------------------------------
/src/useThrottledCallback.ts:
--------------------------------------------------------------------------------
1 | import useDebouncedCallback, {
2 | CallOptions,
3 | DebouncedState,
4 | } from './useDebouncedCallback';
5 |
6 | /**
7 | * Creates a throttled function that only invokes `func` at most once per
8 | * every `wait` milliseconds (or once per browser frame).
9 | *
10 | * The throttled function comes with a `cancel` method to cancel delayed `func`
11 | * invocations and a `flush` method to immediately invoke them.
12 | *
13 | * Provide `options` to indicate whether `func` should be invoked on the leading
14 | * and/or trailing edge of the `wait` timeout. The `func` is invoked with the
15 | * last arguments provided to the throttled function.
16 | *
17 | * Subsequent calls to the throttled function return the result of the last
18 | * `func` invocation.
19 | *
20 | * **Note:** If `leading` and `trailing` options are `true`, `func` is
21 | * invoked on the trailing edge of the timeout only if the throttled function
22 | * is invoked more than once during the `wait` timeout.
23 | *
24 | * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
25 | * until the next tick, similar to `setTimeout` with a timeout of `0`.
26 | *
27 | * If `wait` is omitted in an environment with `requestAnimationFrame`, `func`
28 | * invocation will be deferred until the next frame is drawn (typically about
29 | * 16ms).
30 | *
31 | * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
32 | * for details over the differences between `throttle` and `debounce`.
33 | *
34 | * @category Function
35 | * @param {Function} func The function to throttle.
36 | * @param {number} [wait=0]
37 | * The number of milliseconds to throttle invocations to; if omitted,
38 | * `requestAnimationFrame` is used (if available, otherwise it will be setTimeout(...,0)).
39 | * @param {Object} [options={}] The options object.
40 | * @param {boolean} [options.leading=true]
41 | * Specify invoking on the leading edge of the timeout.
42 | * @param {boolean} [options.trailing=true]
43 | * Specify invoking on the trailing edge of the timeout.
44 | * @returns {Function} Returns the new throttled function.
45 | * @example
46 | *
47 | * // Avoid excessively updating the position while scrolling.
48 | * const scrollHandler = useThrottledCallback(updatePosition, 100)
49 | * window.addEventListener('scroll', scrollHandler)
50 | *
51 | * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes.
52 | * const throttled = useThrottledCallback(renewToken, 300000, { 'trailing': false })
53 | * click
54 | *
55 | * // Cancel the trailing throttled invocation.
56 | * window.addEventListener('popstate', throttled.cancel);
57 | */
58 | export default function useThrottledCallback<
59 | T extends (...args: any) => ReturnType,
60 | >(
61 | func: T,
62 | wait: number,
63 | { leading = true, trailing = true }: CallOptions = {}
64 | ): DebouncedState {
65 | return useDebouncedCallback(func, wait, {
66 | maxWait: wait,
67 | leading,
68 | trailing,
69 | });
70 | }
71 |
--------------------------------------------------------------------------------
/test/useDebounce.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen, act } from '@testing-library/react';
2 | import * as React from 'react';
3 | import useDebounce from '../src/useDebounce';
4 | import { describe, it, expect, jest, beforeEach } from '@jest/globals';
5 |
6 | describe('useDebounce', () => {
7 | beforeEach(() => {
8 | jest.useFakeTimers();
9 | });
10 | it('put initialized value in first render', () => {
11 | function Component() {
12 | const [value] = useDebounce('Hello world', 1000);
13 | return {value}
;
14 | }
15 | render( );
16 | // @ts-ignore
17 | expect(screen.getByRole('test')).toHaveTextContent('Hello world');
18 | });
19 |
20 | it('will update value when timer is called', () => {
21 | function Component({ text }) {
22 | const [value] = useDebounce(text, 1000);
23 | return {value}
;
24 | }
25 | const { rerender } = render( );
26 |
27 | // @ts-ignore
28 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
29 |
30 | rerender( );
31 |
32 | // @ts-ignore
33 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
34 |
35 | act(() => {
36 | jest.runAllTimers();
37 | });
38 | // after runAllTimer text should be updated
39 | // @ts-ignore
40 | expect(screen.getByRole('test')).toHaveTextContent('Hello world');
41 | });
42 |
43 | it('will update value immediately if leading is set to true', () => {
44 | function Component({ text }) {
45 | const [value] = useDebounce(text, 1000, { leading: true });
46 | return {value}
;
47 | }
48 | const tree = render( );
49 |
50 | // check inited value
51 | // @ts-ignore
52 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
53 |
54 | act(() => {
55 | tree.rerender( );
56 | });
57 |
58 | // value should be set immediately by first leading call
59 | // @ts-ignore
60 | expect(screen.getByRole('test')).toHaveTextContent('Hello world');
61 |
62 | act(() => {
63 | tree.rerender( );
64 | });
65 |
66 | // timeout shouldn't have been called yet after leading call was executed
67 | // @ts-ignore
68 | expect(screen.getByRole('test')).toHaveTextContent('Hello world');
69 |
70 | act(() => {
71 | jest.runAllTimers();
72 | });
73 | // final value should update as per last timeout
74 | // @ts-ignore
75 | expect(screen.getByRole('test')).toHaveTextContent('Hello again');
76 | });
77 |
78 | it('will cancel value when cancel method is called', () => {
79 | function Component({ text }) {
80 | const [value, fn] = useDebounce(text, 1000);
81 | setTimeout(fn.cancel, 500);
82 | return {value}
;
83 | }
84 | const tree = render( );
85 |
86 | // @ts-ignore
87 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
88 |
89 | act(() => {
90 | tree.rerender( );
91 | });
92 | // timeout shouldn't have called yet
93 | // @ts-ignore
94 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
95 |
96 | act(() => {
97 | jest.runAllTimers();
98 | });
99 | // after runAllTimer text should not be updated as debounce was cancelled
100 | // @ts-ignore
101 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
102 | });
103 |
104 | it('should apply the latest value', () => {
105 | function Component({ text }) {
106 | const [value] = useDebounce(text, 1000);
107 | return {value}
;
108 | }
109 | const tree = render( );
110 |
111 | // check inited value
112 | // @ts-ignore
113 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
114 |
115 | act(() => {
116 | // this value shouldn't be applied, as we'll set up another one
117 | tree.rerender( );
118 | });
119 | // timeout shouldn't have called yet
120 | // @ts-ignore
121 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
122 |
123 | tree.rerender( );
124 |
125 | act(() => {
126 | jest.runAllTimers();
127 | });
128 | // after runAllTimer text should be updated
129 | // @ts-ignore
130 | expect(screen.getByRole('test')).toHaveTextContent('Right value');
131 | });
132 |
133 | it('should cancel maxWait callback', () => {
134 | function Component({ text }) {
135 | const [value, fn] = useDebounce(text, 500, { maxWait: 600 });
136 | if (text === 'Right value') {
137 | fn.cancel();
138 | }
139 | return {value}
;
140 | }
141 | const tree = render( );
142 |
143 | // check inited value
144 | // @ts-ignore
145 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
146 |
147 | act(() => {
148 | // this value shouldn't be applied, as we'll set up another one
149 | tree.rerender( );
150 | });
151 |
152 | act(() => {
153 | jest.advanceTimersByTime(400);
154 | });
155 |
156 | // timeout shouldn't have called yet
157 | // @ts-ignore
158 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
159 |
160 | act(() => {
161 | tree.rerender( );
162 | });
163 |
164 | act(() => {
165 | jest.advanceTimersByTime(400);
166 | });
167 |
168 | // @ts-ignore
169 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
170 | });
171 |
172 | it('should apply the latest value if maxWait timeout is called', () => {
173 | function Component({ text }) {
174 | const [value] = useDebounce(text, 500, { maxWait: 600 });
175 | return {value}
;
176 | }
177 | const tree = render( );
178 |
179 | // check inited value
180 | // @ts-ignore
181 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
182 |
183 | act(() => {
184 | // this value shouldn't be applied, as we'll set up another one
185 | tree.rerender( );
186 | });
187 |
188 | act(() => {
189 | jest.advanceTimersByTime(400);
190 | });
191 |
192 | // timeout shouldn't have been called yet
193 | // @ts-ignore
194 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
195 |
196 | act(() => {
197 | tree.rerender( );
198 | });
199 |
200 | act(() => {
201 | jest.advanceTimersByTime(400);
202 | });
203 | // after runAllTimer text should be updated
204 | // @ts-ignore
205 | expect(screen.getByRole('test')).toHaveTextContent('Right value');
206 | });
207 |
208 | it("shouldn't apply the previous value if it was changed to started one", () => {
209 | function Component({ text }) {
210 | const [value] = useDebounce(text, 500);
211 | return {value}
;
212 | }
213 |
214 | const tree = render( );
215 |
216 | act(() => {
217 | // this value shouldn't be applied, as we'll set up another one
218 | tree.rerender( );
219 | });
220 |
221 | // timeout shouldn't have been called yet
222 | // @ts-ignore
223 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
224 |
225 | act(() => {
226 | tree.rerender( );
227 | });
228 |
229 | act(() => {
230 | jest.advanceTimersByTime(500);
231 | });
232 |
233 | // Value shouldn't be changed, as we rerender Component with text prop === 'Hello'
234 | // @ts-ignore
235 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
236 | });
237 |
238 | it("shouldn't rerender component for the first time", () => {
239 | function Component({ text }) {
240 | const [value] = useDebounce(text, 1000, { maxWait: 500 });
241 | const rerenderCounter = React.useRef(0);
242 | rerenderCounter.current += 1;
243 | return {rerenderCounter.current}
;
244 | }
245 |
246 | const tree = render( );
247 |
248 | // @ts-ignore
249 | expect(screen.getByRole('test')).toHaveTextContent('1');
250 |
251 | act(() => {
252 | // We wait for the half of maxWait Timeout,
253 | jest.advanceTimersByTime(250);
254 | });
255 |
256 | act(() => {
257 | tree.rerender( );
258 | });
259 |
260 | // @ts-ignore
261 | expect(screen.getByRole('test')).toHaveTextContent('2');
262 |
263 | act(() => {
264 | // We wait for the maxWait Timeout,
265 | jest.advanceTimersByTime(250);
266 | });
267 |
268 | // If maxWait wasn't started at the first render of the component, we shouldn't receive the new value
269 | // @ts-ignore
270 | expect(screen.getByRole('test')).toHaveTextContent('2');
271 | });
272 |
273 | it('should use equality function if supplied', () => {
274 | // Use equality function that always returns true
275 | const eq = jest.fn((left: string, right: string): boolean => {
276 | return true;
277 | });
278 |
279 | function Component({ text }) {
280 | const [value] = useDebounce(text, 1000, { equalityFn: eq });
281 | return {value}
;
282 | }
283 |
284 | const tree = render( );
285 |
286 | expect(eq).toHaveBeenCalledTimes(1);
287 |
288 | act(() => {
289 | tree.rerender( );
290 | });
291 |
292 | expect(eq).toHaveBeenCalledTimes(2);
293 | expect(eq).toHaveBeenCalledWith('Hello', 'Test');
294 | // Since the equality function always returns true, expect the value to stay the same
295 | // @ts-ignore
296 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
297 | });
298 |
299 | it('should setup new value immediately if callPending is called', () => {
300 | let callPending;
301 | function Component({ text }) {
302 | const [value, fn] = useDebounce(text, 1000);
303 | callPending = fn.flush;
304 |
305 | return {value}
;
306 | }
307 |
308 | const tree = render( );
309 |
310 | // @ts-ignore
311 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
312 |
313 | act(() => {
314 | tree.rerender( );
315 | });
316 |
317 | // We don't call neither runTimers no callPending.
318 | // @ts-ignore
319 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
320 |
321 | act(() => {
322 | callPending();
323 | });
324 |
325 | // @ts-ignore
326 | expect(screen.getByRole('test')).toHaveTextContent('Test');
327 | });
328 |
329 | it('should preserve debounced object between re-renders', () => {
330 | let cachedDebounced: unknown = null;
331 | function Component({ text }) {
332 | const [value, debounced] = useDebounce(text, 1000);
333 | if (cachedDebounced == null) {
334 | cachedDebounced = debounced;
335 | } else {
336 | expect(cachedDebounced).toBe(debounced);
337 | }
338 | return {value}
;
339 | }
340 | const tree = render( );
341 |
342 | // check inited value
343 | // @ts-ignore
344 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
345 |
346 | act(() => {
347 | tree.rerender( );
348 | });
349 | // timeout shouldn't have called yet
350 | // @ts-ignore
351 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
352 |
353 | act(() => {
354 | jest.runAllTimers();
355 | });
356 | // after runAllTimer text should be updated
357 | // @ts-ignore
358 | expect(screen.getByRole('test')).toHaveTextContent('Hello world');
359 | });
360 |
361 | it('should change debounced.isPending to true as soon as the function is called in a sync way', () => {
362 | function Component({ text }) {
363 | const [value, { isPending }] = useDebounce(text, 1000);
364 | if (value === text) {
365 | expect(isPending()).toBeFalsy();
366 | } else {
367 | expect(isPending()).toBeTruthy();
368 | }
369 | return {value}
;
370 | }
371 | const tree = render( );
372 |
373 | // check inited value
374 | // @ts-ignore
375 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
376 |
377 | act(() => {
378 | tree.rerender( );
379 | });
380 | // timeout shouldn't have called yet
381 | // @ts-ignore
382 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
383 |
384 | act(() => {
385 | jest.runAllTimers();
386 | });
387 | // after runAllTimer text should be updated
388 | // @ts-ignore
389 | expect(screen.getByRole('test')).toHaveTextContent('Hello world');
390 | });
391 |
392 | it('Should use function as debounced value', () => {
393 | function Component({ fn }) {
394 | const [value] = useDebounce(fn, 1000);
395 | return {value()}
;
396 | }
397 | const tree = render( 'Hello'} />);
398 |
399 | // check inited value
400 | // @ts-ignore
401 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
402 |
403 | act(() => {
404 | tree.rerender( 'Hello world'} />);
405 | });
406 | // timeout shouldn't have called yet
407 | // @ts-ignore
408 | expect(screen.getByRole('test')).toHaveTextContent('Hello');
409 |
410 | act(() => {
411 | jest.runAllTimers();
412 | });
413 | // after runAllTimer text should be updated
414 | // @ts-ignore
415 | expect(screen.getByRole('test')).toHaveTextContent('Hello world');
416 | });
417 |
418 |
419 | it('Handles isPending', () => {
420 | function Component({propValue}) {
421 | const [value, fns] = useDebounce(propValue, 1000);
422 | return (
423 |
424 |
{value}
425 |
{fns.isPending().toString()}
426 |
427 | );
428 | }
429 |
430 | const tree = render( );
431 |
432 | // check inited value
433 | // @ts-ignore
434 | expect(screen.getByRole('value')).toHaveTextContent('Hello');
435 | // @ts-ignore
436 | expect(screen.getByRole('pending')).toHaveTextContent('false');
437 |
438 | act(() => {
439 | tree.rerender( );
440 | });
441 | // timeout shouldn't have called yet
442 | // @ts-ignore
443 | expect(screen.getByRole('value')).toHaveTextContent('Hello');
444 | // @ts-ignore
445 | expect(screen.getByRole('pending')).toHaveTextContent('true');
446 |
447 | act(() => {
448 | jest.runAllTimers();
449 | });
450 | // after runAllTimer text should be updated
451 | // @ts-ignore
452 | expect(screen.getByRole('value')).toHaveTextContent('Hello 1');
453 | // @ts-ignore
454 | expect(screen.getByRole('pending')).toHaveTextContent('false');
455 | })
456 |
457 | it('Should handle isPending state correctly while switching between bounced values', () => {
458 | function Component({propValue}) {
459 | const [value, fns] = useDebounce(propValue, 1000);
460 | return (
461 |
462 |
{value}
463 |
{fns.isPending().toString()}
464 |
465 | );
466 | }
467 |
468 | const tree = render( );
469 |
470 | // check inited value
471 | // @ts-ignore
472 | expect(screen.getByRole('value')).toHaveTextContent('Hello');
473 | // @ts-ignore
474 | expect(screen.getByRole('pending')).toHaveTextContent('false');
475 |
476 | act(() => {
477 | tree.rerender( );
478 | });
479 | // timeout shouldn't have called yet
480 | // @ts-ignore
481 | expect(screen.getByRole('value')).toHaveTextContent('Hello');
482 | // @ts-ignore
483 | expect(screen.getByRole('pending')).toHaveTextContent('true');
484 |
485 | act(() => {
486 | tree.rerender( );
487 | });
488 |
489 | // timeout shouldn't have called yet
490 | // @ts-ignore
491 | expect(screen.getByRole('value')).toHaveTextContent('Hello');
492 | // @ts-ignore
493 | expect(screen.getByRole('pending')).toHaveTextContent('true');
494 |
495 | act(() => {
496 | jest.runAllTimers();
497 | });
498 | // after runAllTimer text should be updated
499 | // @ts-ignore
500 | expect(screen.getByRole('value')).toHaveTextContent('Hello');
501 | // @ts-ignore
502 | expect(screen.getByRole('pending')).toHaveTextContent('false');
503 | })
504 | });
505 |
--------------------------------------------------------------------------------
/test/useDebouncedCallback.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen, act, fireEvent } from '@testing-library/react';
2 | import { useEffect, useCallback, useRef } from 'react';
3 | import * as React from 'react';
4 | import useDebouncedCallback from '../src/useDebouncedCallback';
5 | import { describe, it, expect, jest, beforeEach, test } from '@jest/globals';
6 |
7 | describe('useDebouncedCallback', () => {
8 | beforeEach(() => {
9 | jest.useFakeTimers();
10 | });
11 | it('will call callback when timeout is called', () => {
12 | const callback = jest.fn();
13 |
14 | function Component() {
15 | const debounced = useDebouncedCallback(callback, 1000);
16 | debounced();
17 | return null;
18 | }
19 | render( );
20 |
21 | expect(callback.mock.calls.length).toBe(0);
22 |
23 | act(() => {
24 | jest.runAllTimers();
25 | });
26 |
27 | expect(callback.mock.calls.length).toBe(1);
28 | });
29 |
30 | it('will call leading callback immediately (but only once, as trailing is set to false)', () => {
31 | const callback = jest.fn();
32 |
33 | function Component() {
34 | const debounced = useDebouncedCallback(callback, 1000, {
35 | leading: true,
36 | trailing: false,
37 | });
38 | debounced();
39 | return null;
40 | }
41 | render( );
42 |
43 | expect(callback.mock.calls.length).toBe(1);
44 |
45 | act(() => {
46 | jest.runAllTimers();
47 | });
48 |
49 | expect(callback.mock.calls.length).toBe(1);
50 | });
51 |
52 | it('will call leading callback as well as next debounced call', () => {
53 | const callback = jest.fn();
54 |
55 | function Component() {
56 | const debounced = useDebouncedCallback(callback, 1000, { leading: true });
57 | debounced();
58 | debounced();
59 | return null;
60 | }
61 | render( );
62 |
63 | expect(callback.mock.calls.length).toBe(1);
64 |
65 | act(() => {
66 | jest.runAllTimers();
67 | });
68 |
69 | expect(callback.mock.calls.length).toBe(2);
70 | });
71 |
72 | it('will call three callbacks if no debounced callbacks are pending', () => {
73 | const callback = jest.fn();
74 |
75 | function Component() {
76 | const debounced = useDebouncedCallback(callback, 1000, { leading: true });
77 | debounced();
78 | debounced();
79 | setTimeout(() => {
80 | debounced();
81 | }, 1001);
82 | return null;
83 | }
84 | render( );
85 |
86 | expect(callback.mock.calls.length).toBe(1);
87 |
88 | act(() => {
89 | jest.advanceTimersByTime(1001);
90 | });
91 |
92 | expect(callback.mock.calls.length).toBe(3);
93 | });
94 |
95 | it('Subsequent calls to the debounced function `debounced.callback` return the result of the last func invocation.', () => {
96 | const callback = jest.fn(() => 42);
97 |
98 | let callbackCache;
99 | function Component() {
100 | const debounced = useDebouncedCallback(callback, 1000);
101 | callbackCache = debounced;
102 | return null;
103 | }
104 | render( );
105 |
106 | const result = callbackCache();
107 | expect(callback.mock.calls.length).toBe(0);
108 | expect(result).toBeUndefined();
109 |
110 | act(() => {
111 | jest.runAllTimers();
112 | });
113 | expect(callback.mock.calls.length).toBe(1);
114 | const subsequentResult = callbackCache();
115 |
116 | expect(callback.mock.calls.length).toBe(1);
117 | expect(subsequentResult).toBe(42);
118 | });
119 |
120 | it('will call a second leading callback if no debounced callbacks are pending with trailing false', () => {
121 | const callback = jest.fn();
122 |
123 | function Component() {
124 | const debounced = useDebouncedCallback(callback, 1000, {
125 | leading: true,
126 | trailing: false,
127 | });
128 | debounced();
129 | setTimeout(() => {
130 | debounced();
131 | }, 1001);
132 | return null;
133 | }
134 | render( );
135 |
136 | expect(callback.mock.calls.length).toBe(1);
137 |
138 | act(() => {
139 | jest.advanceTimersByTime(1001);
140 | });
141 |
142 | expect(callback.mock.calls.length).toBe(2);
143 | });
144 |
145 | it("won't call both on the leading edge and on the trailing edge if leading and trailing are set up to true and function call is only once", () => {
146 | const callback = jest.fn();
147 |
148 | function Component() {
149 | // trailing is true by default
150 | const debounced = useDebouncedCallback(callback, 1000, { leading: true });
151 |
152 | debounced();
153 | return null;
154 | }
155 | render( );
156 |
157 | expect(callback.mock.calls.length).toBe(1);
158 |
159 | act(() => {
160 | jest.runAllTimers();
161 | });
162 |
163 | expect(callback.mock.calls.length).toBe(1);
164 | });
165 |
166 | it('will call both on the leading edge and on the trailing edge if leading and trailing are set up to true and there are more than 1 function call', () => {
167 | const callback = jest.fn();
168 |
169 | function Component() {
170 | // trailing is true by default
171 | const debounced = useDebouncedCallback(callback, 1000, { leading: true });
172 | debounced();
173 | debounced();
174 | return null;
175 | }
176 | render( );
177 |
178 | expect(callback.mock.calls.length).toBe(1);
179 |
180 | act(() => {
181 | jest.runAllTimers();
182 | });
183 |
184 | expect(callback.mock.calls.length).toBe(2);
185 | });
186 |
187 | test.each`
188 | options | _0 | _190 | _200 | _210 | _500
189 | ${{ leading: true, trailing: true }} | ${1} | ${1} | ${1} | ${1} | ${2}
190 | ${{ leading: true, trailing: false }} | ${1} | ${1} | ${1} | ${1} | ${1}
191 | ${{ leading: false, trailing: true }} | ${0} | ${0} | ${0} | ${0} | ${1}
192 | ${{ leading: false, trailing: false }} | ${0} | ${0} | ${0} | ${0} | ${0}
193 | ${{ leading: true, trailing: true, maxWait: 190 }} | ${1} | ${1} | ${2} | ${2} | ${3}
194 | ${{ leading: true, trailing: false, maxWait: 190 }} | ${1} | ${1} | ${1} | ${2} | ${2}
195 | ${{ leading: false, trailing: true, maxWait: 190 }} | ${0} | ${0} | ${1} | ${1} | ${2}
196 | ${{ leading: true, trailing: true, maxWait: 200 }} | ${1} | ${1} | ${2} | ${2} | ${3}
197 | ${{ leading: true, trailing: false, maxWait: 200 }} | ${1} | ${1} | ${1} | ${2} | ${2}
198 | ${{ leading: false, trailing: true, maxWait: 200 }} | ${0} | ${0} | ${1} | ${1} | ${2}
199 | ${{ leading: false, trailing: false, maxWait: 200 }} | ${0} | ${0} | ${0} | ${0} | ${0}
200 | ${{ leading: true, trailing: true, maxWait: 210 }} | ${1} | ${1} | ${1} | ${2} | ${3}
201 | ${{ leading: true, trailing: false, maxWait: 210 }} | ${1} | ${1} | ${1} | ${1} | ${2}
202 | ${{ leading: false, trailing: true, maxWait: 210 }} | ${0} | ${0} | ${0} | ${1} | ${2}
203 | `('options=$options', ({ options, _0, _190, _200, _210, _500 }) => {
204 | const callback = jest.fn();
205 |
206 | function Component() {
207 | // @ts-ignore
208 | const debounced = useDebouncedCallback(callback, 200, options);
209 |
210 | debounced();
211 | expect(callback.mock.calls.length).toBe(_0);
212 |
213 | setTimeout(() => {
214 | expect(callback.mock.calls.length).toBe(_190);
215 | debounced();
216 | }, 191);
217 |
218 | setTimeout(() => {
219 | expect(callback.mock.calls.length).toBe(_200);
220 | debounced();
221 | }, 201);
222 |
223 | setTimeout(() => {
224 | expect(callback.mock.calls.length).toBe(_210);
225 | debounced();
226 | }, 211);
227 |
228 | setTimeout(() => {
229 | expect(callback.mock.calls.length).toBe(_500);
230 | }, 500);
231 |
232 | return null;
233 | }
234 | render( );
235 |
236 | act(() => {
237 | jest.runAllTimers();
238 | });
239 | });
240 |
241 | it('will call callback only with the latest params', () => {
242 | const callback = jest.fn((param) => {
243 | expect(param).toBe('Right param');
244 | });
245 |
246 | function Component() {
247 | const debounced = useDebouncedCallback(callback, 1000);
248 | debounced('Wrong param');
249 | setTimeout(() => {
250 | debounced('Right param');
251 | }, 500);
252 | return null;
253 | }
254 | render( );
255 |
256 | act(() => {
257 | jest.advanceTimersByTime(500);
258 | });
259 | expect(callback.mock.calls.length).toBe(0);
260 |
261 | act(() => {
262 | jest.advanceTimersByTime(1000);
263 | });
264 |
265 | expect(callback.mock.calls.length).toBe(1);
266 | });
267 |
268 | it('will cancel delayed callback when cancel method is called', () => {
269 | const callback = jest.fn();
270 |
271 | function Component() {
272 | const debounced = useDebouncedCallback(callback, 1000);
273 | debounced();
274 | setTimeout(debounced.cancel, 500);
275 | return null;
276 | }
277 | render( );
278 |
279 | act(() => {
280 | jest.runAllTimers();
281 | });
282 |
283 | expect(callback.mock.calls.length).toBe(0);
284 | });
285 |
286 | it('will change callback function, if params from dependencies has changed', () => {
287 | function Component({ text }) {
288 | const debounced = useDebouncedCallback(
289 | useCallback(
290 | jest.fn(() => {
291 | expect(text).toBe('Right param');
292 | }),
293 | [text]
294 | ),
295 | 1000
296 | );
297 | return ;
298 | }
299 | const tree = render( );
300 |
301 | tree.rerender( );
302 |
303 | act(() => {
304 | fireEvent.click(screen.getByRole('click'));
305 | });
306 |
307 | act(() => {
308 | jest.runAllTimers();
309 | });
310 | });
311 |
312 | it("won't change callback function, if params from dependencies hasn't changed", () => {
313 | function Component({ text }) {
314 | const debounced = useDebouncedCallback(
315 | useCallback(
316 | jest.fn(() => {
317 | expect(text).toBe('Right param');
318 | }),
319 | []
320 | ),
321 | 1000
322 | );
323 | return ;
324 | }
325 | const tree = render( );
326 |
327 | tree.rerender( );
328 |
329 | fireEvent.click(screen.getByRole('click'));
330 |
331 | act(() => {
332 | jest.runAllTimers();
333 | });
334 | });
335 |
336 | it('call callback with the latest value if maxWait time exceed', () => {
337 | const callback = (value) => expect(value).toBe('Right value');
338 |
339 | function Component({ text }) {
340 | const debounced = useDebouncedCallback(callback, 500, { maxWait: 600 });
341 | debounced(text);
342 | return {text} ;
343 | }
344 | const tree = render( );
345 |
346 | act(() => {
347 | jest.advanceTimersByTime(400);
348 | tree.rerender( );
349 | });
350 |
351 | act(() => {
352 | jest.advanceTimersByTime(400);
353 | });
354 | });
355 |
356 | it('will call callback if maxWait time exceed', () => {
357 | const callback = jest.fn();
358 |
359 | function Component({ text }) {
360 | const debounced = useDebouncedCallback(callback, 500, { maxWait: 600 });
361 | debounced();
362 | return {text} ;
363 | }
364 | const tree = render( );
365 |
366 | expect(callback.mock.calls.length).toBe(0);
367 | // @ts-ignore
368 | expect(screen.getByRole('test')).toHaveTextContent('one');
369 |
370 | act(() => {
371 | jest.advanceTimersByTime(400);
372 | tree.rerender( );
373 | });
374 |
375 | expect(callback.mock.calls.length).toBe(0);
376 | // @ts-ignore
377 | expect(screen.getByRole('test')).toHaveTextContent('test');
378 |
379 | act(() => {
380 | jest.advanceTimersByTime(400);
381 | });
382 |
383 | expect(callback.mock.calls.length).toBe(1);
384 | });
385 |
386 | it('will cancel callback if maxWait time exceed and cancel method was invoked', () => {
387 | const callback = jest.fn();
388 |
389 | function Component({ text }) {
390 | const debounced = useDebouncedCallback(callback, 500, { maxWait: 600 });
391 | debounced();
392 | if (text === 'test') {
393 | debounced.cancel();
394 | }
395 | return {text} ;
396 | }
397 | const tree = render( );
398 |
399 | expect(callback.mock.calls.length).toBe(0);
400 | // @ts-ignore
401 | expect(screen.getByRole('test')).toHaveTextContent('one');
402 |
403 | act(() => {
404 | jest.advanceTimersByTime(400);
405 | // @ts-ignore
406 | expect(screen.getByRole('test')).toHaveTextContent('one');
407 | });
408 |
409 | expect(callback.mock.calls.length).toBe(0);
410 | tree.rerender( );
411 |
412 | act(() => {
413 | jest.advanceTimersByTime(400);
414 | });
415 |
416 | expect(callback.mock.calls.length).toBe(0);
417 | });
418 |
419 | it('will call pending callback if callPending function is called', () => {
420 | const callback = jest.fn();
421 |
422 | function Component({ text }) {
423 | const debounced = useDebouncedCallback(callback, 500);
424 | debounced();
425 | if (text === 'test') {
426 | debounced.flush();
427 | }
428 | return {text} ;
429 | }
430 | const tree = render( );
431 |
432 | expect(callback.mock.calls.length).toBe(0);
433 | // @ts-ignore
434 | expect(screen.getByRole('test')).toHaveTextContent('one');
435 |
436 | act(() => {
437 | tree.rerender( );
438 | });
439 |
440 | expect(callback.mock.calls.length).toBe(1);
441 | });
442 |
443 | it('won\t call pending callback if callPending function is called and there are no items in queue', () => {
444 | const callback = jest.fn();
445 |
446 | function Component({ text }) {
447 | const debounced = useDebouncedCallback(callback, 500);
448 | if (text === 'test') {
449 | debounced.flush();
450 | }
451 | return {text} ;
452 | }
453 | const tree = render( );
454 |
455 | expect(callback.mock.calls.length).toBe(0);
456 | // @ts-ignore
457 | expect(screen.getByRole('test')).toHaveTextContent('one');
458 |
459 | act(() => {
460 | tree.rerender( );
461 | });
462 |
463 | expect(callback.mock.calls.length).toBe(0);
464 | // @ts-ignore
465 | expect(screen.getByRole('test')).toHaveTextContent('test');
466 | });
467 |
468 | it('won\t call pending callback if callPending function is called and cancel method is also executed', () => {
469 | const callback = jest.fn();
470 |
471 | function Component({ text }) {
472 | const debounced = useDebouncedCallback(callback, 500);
473 | debounced();
474 | if (text === 'test') {
475 | debounced.cancel();
476 | debounced.flush();
477 | }
478 | return {text} ;
479 | }
480 | const tree = render( );
481 |
482 | expect(callback.mock.calls.length).toBe(0);
483 | // @ts-ignore
484 | expect(screen.getByRole('test')).toHaveTextContent('one');
485 |
486 | act(() => {
487 | tree.rerender( );
488 | });
489 |
490 | expect(callback.mock.calls.length).toBe(0);
491 | // @ts-ignore
492 | expect(screen.getByRole('test')).toHaveTextContent('test');
493 | });
494 |
495 | it('will call pending callback if callPending function is called on component unmount', () => {
496 | const callback = jest.fn();
497 |
498 | function Component({ text }) {
499 | const debounced = useDebouncedCallback(callback, 500);
500 |
501 | debounced();
502 | useEffect(() => {
503 | return () => { debounced.flush(); };
504 | }, []);
505 | return {text} ;
506 | }
507 | const tree = render( );
508 |
509 | expect(callback.mock.calls.length).toBe(0);
510 | // @ts-ignore
511 | expect(screen.getByRole('test')).toHaveTextContent('one');
512 |
513 | act(() => {
514 | tree.unmount();
515 | });
516 |
517 | expect(callback.mock.calls.length).toBe(1);
518 | });
519 |
520 | it('will memoize debouncedCallback', () => {
521 | let debouncedCallbackCached: any = null;
522 |
523 | function Component({ text }) {
524 | const debounced = useDebouncedCallback(
525 | useCallback(() => {}, []),
526 | 500
527 | );
528 |
529 | if (debouncedCallbackCached) {
530 | expect(debounced).toBe(debouncedCallbackCached);
531 | }
532 | debouncedCallbackCached = debounced;
533 |
534 | return {text} ;
535 | }
536 | const tree = render( );
537 |
538 | // @ts-ignore
539 | expect(screen.getByRole('test')).toHaveTextContent('one');
540 |
541 | act(() => {
542 | tree.rerender( );
543 | });
544 | });
545 |
546 | it('will change reference to debouncedCallback timeout is changed', () => {
547 | expect.assertions(3);
548 | let debouncedCallbackCached: any = null;
549 | let timeoutCached = null;
550 |
551 | function Component({ text, timeout }) {
552 | const debounced = useDebouncedCallback(
553 | useCallback(() => {}, [text]),
554 | timeout
555 | );
556 |
557 | if (debouncedCallbackCached) {
558 | if (timeoutCached === timeout) {
559 | expect(debounced).toBe(debouncedCallbackCached);
560 | } else {
561 | expect(debounced).not.toBe(debouncedCallbackCached);
562 | }
563 | }
564 | debouncedCallbackCached = debounced;
565 | timeoutCached = timeout;
566 |
567 | return {text} ;
568 | }
569 | const tree = render( );
570 |
571 | // @ts-ignore
572 | expect(screen.getByRole('test')).toHaveTextContent('one');
573 |
574 | act(() => {
575 | tree.rerender( );
576 | });
577 |
578 | act(() => {
579 | tree.rerender( );
580 | });
581 | });
582 |
583 | it('will call the latest callback', () => {
584 | expect.assertions(1);
585 |
586 | function Component({ callback }) {
587 | const debounced = useDebouncedCallback(callback, 500);
588 | const counter = useRef(1);
589 |
590 | useEffect(() => {
591 | // this useEffect should be called only once
592 | debounced(counter.current);
593 |
594 | counter.current = counter.current + 1;
595 | }, [debounced]);
596 |
597 | return null;
598 | }
599 | const tree = render(
600 | {
602 | throw new Error("This callback shouldn't be executed");
603 | }}
604 | />
605 | );
606 |
607 | act(() => {
608 | tree.rerender(
609 | {
611 | // This callback should be called with counter === 1
612 | expect(counter).toBe(1);
613 | }}
614 | />
615 | );
616 | });
617 |
618 | jest.advanceTimersByTime(500);
619 | });
620 |
621 | it('will change reference to debouncedCallback if maxWait or delay option is changed', () => {
622 | expect.assertions(5);
623 | let debouncedCallbackCached: any = null;
624 | let cachedObj: any = null;
625 |
626 | function Component({ text, maxWait = 1000, delay = 500 }) {
627 | const debounced = useDebouncedCallback(
628 | useCallback(() => {}, []),
629 | delay,
630 | { maxWait }
631 | );
632 |
633 | if (debouncedCallbackCached) {
634 | if (cachedObj.delay === delay && cachedObj.maxWait === maxWait) {
635 | expect(debounced).toBe(debouncedCallbackCached);
636 | } else {
637 | expect(debounced).not.toBe(debouncedCallbackCached);
638 | }
639 | }
640 | debouncedCallbackCached = debounced;
641 | cachedObj = { text, maxWait, delay };
642 |
643 | return {text} ;
644 | }
645 | const tree = render( );
646 |
647 | // @ts-ignore
648 | expect(screen.getByRole('test')).toHaveTextContent('one');
649 |
650 | act(() => {
651 | tree.rerender( );
652 | });
653 |
654 | act(() => {
655 | tree.rerender( );
656 | });
657 |
658 | act(() => {
659 | tree.rerender( );
660 | });
661 |
662 | act(() => {
663 | tree.rerender( );
664 | });
665 | });
666 |
667 | it('will memoize callPending', () => {
668 | let callPendingCached: any = null;
669 |
670 | function Component({ text }) {
671 | const debounced = useDebouncedCallback(
672 | useCallback(() => {}, []),
673 | 500
674 | );
675 |
676 | if (callPendingCached) {
677 | expect(debounced.flush).toBe(callPendingCached);
678 | }
679 | callPendingCached = debounced.flush;
680 |
681 | return {text} ;
682 | }
683 | const tree = render( );
684 |
685 | // @ts-ignore
686 | expect(screen.getByRole('test')).toHaveTextContent('one');
687 |
688 | act(() => {
689 | tree.rerender( );
690 | });
691 | });
692 |
693 | it('will memoize debounced object', () => {
694 | let cached: any = null;
695 |
696 | function Component({ text }) {
697 | const debounced = useDebouncedCallback(
698 | useCallback(() => {}, []),
699 | 500
700 | );
701 |
702 | if (cached) {
703 | expect(debounced).toBe(cached);
704 | }
705 | cached = debounced;
706 |
707 | return {text} ;
708 | }
709 | const tree = render( );
710 |
711 | // @ts-ignore
712 | expect(screen.getByRole('test')).toHaveTextContent('one');
713 |
714 | act(() => {
715 | tree.rerender( );
716 | });
717 | });
718 |
719 | it('pending indicates whether we have pending callbacks', () => {
720 | function Component({ text }) {
721 | const debounced = useDebouncedCallback(
722 | useCallback(() => {}, []),
723 | 500
724 | );
725 |
726 | expect(debounced.isPending()).toBeFalsy();
727 | debounced();
728 | expect(debounced.isPending()).toBeTruthy();
729 | debounced.flush();
730 | expect(debounced.isPending()).toBeFalsy();
731 |
732 | return {text} ;
733 | }
734 | render( );
735 | });
736 | });
737 |
--------------------------------------------------------------------------------
/test/useThrottledCallback.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, act } from '@testing-library/react';
2 | import * as React from 'react';
3 | import useThrottledCallback from '../src/useThrottledCallback';
4 | import { describe, it, expect, jest, beforeEach, test } from '@jest/globals';
5 |
6 | describe('useThrottledCallback', () => {
7 | beforeEach(() => {
8 | jest.useFakeTimers();
9 | });
10 | it('will call callback when timeout is called', () => {
11 | const callback = jest.fn();
12 |
13 | function Component() {
14 | const debounced = useThrottledCallback(callback, 1000, {
15 | leading: false,
16 | trailing: true,
17 | });
18 | debounced();
19 | return null;
20 | }
21 | render( );
22 |
23 | expect(callback.mock.calls.length).toBe(0);
24 |
25 | act(() => {
26 | jest.runAllTimers();
27 | });
28 |
29 | expect(callback.mock.calls.length).toBe(1);
30 | });
31 |
32 | it('will call leading callback immediately (but only once, as trailing is set to false)', () => {
33 | const callback = jest.fn();
34 |
35 | function Component() {
36 | const debounced = useThrottledCallback(callback, 1000, {
37 | leading: true,
38 | trailing: false,
39 | });
40 | debounced();
41 | return null;
42 | }
43 | render( );
44 |
45 | expect(callback.mock.calls.length).toBe(1);
46 |
47 | act(() => {
48 | jest.runAllTimers();
49 | });
50 |
51 | expect(callback.mock.calls.length).toBe(1);
52 | });
53 |
54 | it('will call leading callback as well as next debounced call', () => {
55 | const callback = jest.fn();
56 |
57 | function Component() {
58 | const debounced = useThrottledCallback(callback, 1000, { leading: true });
59 | debounced();
60 | debounced();
61 | return null;
62 | }
63 | render( );
64 |
65 | expect(callback.mock.calls.length).toBe(1);
66 |
67 | act(() => {
68 | jest.runAllTimers();
69 | });
70 |
71 | expect(callback.mock.calls.length).toBe(2);
72 | });
73 |
74 | it('will call three callbacks if no debounced callbacks are pending', () => {
75 | const callback = jest.fn();
76 |
77 | function Component() {
78 | const debounced = useThrottledCallback(callback, 1000, { leading: true });
79 | debounced();
80 | debounced();
81 | setTimeout(() => {
82 | debounced();
83 | }, 1001);
84 | return null;
85 | }
86 | render( );
87 |
88 | expect(callback.mock.calls.length).toBe(1);
89 |
90 | act(() => {
91 | jest.advanceTimersByTime(1001);
92 | });
93 |
94 | expect(callback.mock.calls.length).toBe(3);
95 | });
96 | });
97 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "esModuleInterop": true,
5 | "target": "ESNext",
6 | "module": "ESNext",
7 | "moduleResolution": "node",
8 | "lib": ["es5", "es2015", "dom"],
9 | "jsx": "react",
10 | "declaration": true
11 | },
12 | "include": ["src/**/*"],
13 | "exclude": ["node_modules", "test"]
14 | }
15 |
--------------------------------------------------------------------------------