extra-react-hooks 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -63,7 +63,7 @@ function useUnmount(effect: () => void): void
63
63
  ```ts
64
64
  function useEffectAsync(
65
65
  effect: (signal: AbortSignal) => Promise<void>
66
- , deps?: DependencyList
66
+ , deps?: React.DependencyList
67
67
  ): void
68
68
  ```
69
69
 
@@ -71,7 +71,7 @@ function useEffectAsync(
71
71
  ```ts
72
72
  function useAbortableCallback<Args extends unknown[], Result>(
73
73
  callback: (...args: [...args: Args, signal: AbortSignal]) => PromiseLike<Result>
74
- , deps: DependencyList
74
+ , deps: React.DependencyList
75
75
  ): (...args: [...args: Args, signal: AbortSignal | Falsy]) => Promise<Result>
76
76
  ```
77
77
 
@@ -190,6 +190,32 @@ function useGetSet<T>(
190
190
  ```ts
191
191
  function useIsomorphicLayoutEffect(
192
192
  effect: EffectCallback
193
- , deps?: DependencyList
193
+ , deps?: React.DependencyList
194
194
  ): void
195
195
  ```
196
+
197
+ ### useDebounce
198
+ ```ts
199
+ function useDebounce<Args extends unknown[]>(
200
+ fn: (...args: Args) => void
201
+ , timeout: number
202
+ , deps?: React.DependencyList
203
+ ): (...args: Args) => void
204
+ ```
205
+
206
+ ### useNumberInput
207
+ ```ts
208
+ function useNumberInput(params: {
209
+ value: number
210
+ lazy?: boolean
211
+
212
+ onChange(value: number): void
213
+ }): {
214
+ getInputProps(): Required<Pick<
215
+ React.ComponentPropsWithoutRef<'input'>
216
+ , 'value'
217
+ | 'onChange'
218
+ | 'onBlur'
219
+ >>
220
+ }
221
+ ```
package/lib/index.d.ts CHANGED
@@ -21,3 +21,5 @@ export * from './use-iife.js';
21
21
  export * from './use-memo-with-cleanup.js';
22
22
  export * from './use-get-set.js';
23
23
  export * from './use-isomorphic-layout-effect.js';
24
+ export * from './use-debounce.js';
25
+ export * from './use-number-input.js';
package/lib/index.js CHANGED
@@ -21,4 +21,6 @@ export * from './use-iife.js';
21
21
  export * from './use-memo-with-cleanup.js';
22
22
  export * from './use-get-set.js';
23
23
  export * from './use-isomorphic-layout-effect.js';
24
+ export * from './use-debounce.js';
25
+ export * from './use-number-input.js';
24
26
  //# sourceMappingURL=index.js.map
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA;AACnC,cAAc,0BAA0B,CAAA;AACxC,cAAc,yBAAyB,CAAA;AACvC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,uBAAuB,CAAA;AACrC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,wBAAwB,CAAA;AACtC,cAAc,2BAA2B,CAAA;AACzC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,0BAA0B,CAAA;AACxC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA;AACrC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,eAAe,CAAA;AAC7B,cAAc,4BAA4B,CAAA;AAC1C,cAAc,kBAAkB,CAAA;AAChC,cAAc,mCAAmC,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA;AACnC,cAAc,0BAA0B,CAAA;AACxC,cAAc,yBAAyB,CAAA;AACvC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,uBAAuB,CAAA;AACrC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,wBAAwB,CAAA;AACtC,cAAc,2BAA2B,CAAA;AACzC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,0BAA0B,CAAA;AACxC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA;AACrC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,eAAe,CAAA;AAC7B,cAAc,4BAA4B,CAAA;AAC1C,cAAc,kBAAkB,CAAA;AAChC,cAAc,mCAAmC,CAAA;AACjD,cAAc,mBAAmB,CAAA;AACjC,cAAc,uBAAuB,CAAA"}
@@ -1,3 +1,2 @@
1
- import { DependencyList } from 'react';
2
1
  import { Falsy } from '@blackglory/prelude';
3
- export declare function useAbortableCallback<Args extends unknown[], Result>(callback: (...args: [...args: Args, signal: AbortSignal]) => PromiseLike<Result>, deps: DependencyList): (...args: [...args: Args, signal: AbortSignal | Falsy]) => Promise<Result>;
2
+ export declare function useAbortableCallback<Args extends unknown[], Result>(callback: (...args: [...args: Args, signal: AbortSignal]) => PromiseLike<Result>, deps: React.DependencyList): (...args: [...args: Args, signal: AbortSignal | Falsy]) => Promise<Result>;
@@ -1 +1 @@
1
- {"version":3,"file":"use-abortable-callback.js","sourceRoot":"","sources":["../src/use-abortable-callback.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAkB,MAAM,OAAO,CAAA;AACnD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAG/D,MAAM,UAAU,oBAAoB,CAClC,QAAgF,EAChF,IAAoB;IAEpB,MAAM,UAAU,GAAG,kBAAkB,CACnC,GAAG,EAAE,CAAC,IAAI,eAAe,EAAE,EAC3B,UAAU,CAAC,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAChC,IAAI,CACL,CAAA;IAED,OAAO,WAAW,CAAC,KAAK,EAAE,GAAG,IAAkD,EAAE,EAAE;QACjF,MAAM,MAAM,GAAG,gBAAgB,CAAC;YAC9B,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAwB;YAC5C,UAAU,CAAC,MAAM;SAClB,CAAC,CAAA;QACF,MAAM,CAAC,cAAc,EAAE,CAAA;QAEvB,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAS,CAAA;QAE1C,OAAO,MAAM,QAAQ,CAAC,GAAG,QAAQ,EAAE,MAAM,CAAC,CAAA;IAC5C,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,CAAC,CAAA;AAC3B,CAAC"}
1
+ {"version":3,"file":"use-abortable-callback.js","sourceRoot":"","sources":["../src/use-abortable-callback.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AACnC,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAG/D,MAAM,UAAU,oBAAoB,CAClC,QAAgF,EAChF,IAA0B;IAE1B,MAAM,UAAU,GAAG,kBAAkB,CACnC,GAAG,EAAE,CAAC,IAAI,eAAe,EAAE,EAC3B,UAAU,CAAC,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAChC,IAAI,CACL,CAAA;IAED,OAAO,WAAW,CAAC,KAAK,EAAE,GAAG,IAAkD,EAAE,EAAE;QACjF,MAAM,MAAM,GAAG,gBAAgB,CAAC;YAC9B,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAwB;YAC5C,UAAU,CAAC,MAAM;SAClB,CAAC,CAAA;QACF,MAAM,CAAC,cAAc,EAAE,CAAA;QAEvB,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAS,CAAA;QAE1C,OAAO,MAAM,QAAQ,CAAC,GAAG,QAAQ,EAAE,MAAM,CAAC,CAAA;IAC5C,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,CAAC,CAAA;AAC3B,CAAC"}
@@ -0,0 +1 @@
1
+ export declare function useDebounce<Args extends unknown[]>(fn: (...args: Args) => void, timeout: number, deps?: React.DependencyList): (...args: Args) => void;
@@ -0,0 +1,18 @@
1
+ import { useRef, useEffect } from 'react';
2
+ import { setTimeout } from 'extra-timers';
3
+ export function useDebounce(fn, timeout, deps) {
4
+ const fnRef = useRef(fn);
5
+ const cancelTimeoutRef = useRef(undefined);
6
+ useEffect(() => {
7
+ fnRef.current = fn;
8
+ }, deps);
9
+ return (...args) => {
10
+ var _a;
11
+ (_a = cancelTimeoutRef.current) === null || _a === void 0 ? void 0 : _a.call(cancelTimeoutRef);
12
+ cancelTimeoutRef.current = setTimeout(timeout, () => {
13
+ fnRef.current(...args);
14
+ cancelTimeoutRef.current = undefined;
15
+ });
16
+ };
17
+ }
18
+ //# sourceMappingURL=use-debounce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-debounce.js","sourceRoot":"","sources":["../src/use-debounce.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAEzC,MAAM,UAAU,WAAW,CACzB,EAA2B,EAC3B,OAAe,EACf,IAA2B;IAE3B,MAAM,KAAK,GAAG,MAAM,CAA0B,EAAE,CAAC,CAAA;IACjD,MAAM,gBAAgB,GAAG,MAAM,CAAa,SAAS,CAAC,CAAA;IAEtD,SAAS,CAAC,GAAG,EAAE;QACb,KAAK,CAAC,OAAO,GAAG,EAAE,CAAA;IACpB,CAAC,EAAE,IAAI,CAAC,CAAA;IAER,OAAO,CAAC,GAAG,IAAU,EAAQ,EAAE;;QAC7B,MAAA,gBAAgB,CAAC,OAAO,gEAAI,CAAA;QAE5B,gBAAgB,CAAC,OAAO,GAAG,UAAU,CAAC,OAAO,EAAE,GAAG,EAAE;YAClD,KAAK,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,CAAA;YAEtB,gBAAgB,CAAC,OAAO,GAAG,SAAS,CAAA;QACtC,CAAC,CAAC,CAAA;IACJ,CAAC,CAAA;AACH,CAAC"}
@@ -1,2 +1 @@
1
- import { DependencyList } from 'react';
2
- export declare function useEffectAsync(effect: (signal: AbortSignal) => Promise<void>, deps?: DependencyList): void;
1
+ export declare function useEffectAsync(effect: (signal: AbortSignal) => Promise<void>, deps?: React.DependencyList): void;
@@ -1 +1 @@
1
- {"version":3,"file":"use-effect-async.js","sourceRoot":"","sources":["../src/use-effect-async.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAkB,MAAM,OAAO,CAAA;AACjD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAA;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAEhD,MAAM,kBAAmB,SAAQ,WAAW;CAAG;AAE/C,MAAM,UAAU,cAAc,CAC5B,MAA8C,EAC9C,IAAqB;IAErB,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;QAExC,EAAE,CAAC,KAAK,IAAI,EAAE;YACZ,IAAI,CAAC;gBACH,UAAU,CAAC,MAAM,CAAC,cAAc,EAAE,CAAA;gBAElC,MAAM,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;YACjC,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,GAAG,YAAY,kBAAkB,EAAE,CAAC;oBACtC,IAAI,EAAE,CAAA;gBACR,CAAC;qBAAM,CAAC;oBACN,MAAM,GAAG,CAAA;gBACX,CAAC;YACH,CAAC;QACH,CAAC,CAAC,CAAA;QAEF,OAAO,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,kBAAkB,EAAE,CAAC,CAAA;IACzD,CAAC,EAAE,IAAI,CAAC,CAAA;AACV,CAAC"}
1
+ {"version":3,"file":"use-effect-async.js","sourceRoot":"","sources":["../src/use-effect-async.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACjC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAA;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAEhD,MAAM,kBAAmB,SAAQ,WAAW;CAAG;AAE/C,MAAM,UAAU,cAAc,CAC5B,MAA8C,EAC9C,IAA2B;IAE3B,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;QAExC,EAAE,CAAC,KAAK,IAAI,EAAE;YACZ,IAAI,CAAC;gBACH,UAAU,CAAC,MAAM,CAAC,cAAc,EAAE,CAAA;gBAElC,MAAM,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;YACjC,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,GAAG,YAAY,kBAAkB,EAAE,CAAC;oBACtC,IAAI,EAAE,CAAA;gBACR,CAAC;qBAAM,CAAC;oBACN,MAAM,GAAG,CAAA;gBACX,CAAC;YACH,CAAC;QACH,CAAC,CAAC,CAAA;QAEF,OAAO,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,kBAAkB,EAAE,CAAC,CAAA;IACzD,CAAC,EAAE,IAAI,CAAC,CAAA;AACV,CAAC"}
@@ -0,0 +1,7 @@
1
+ export declare function useNumberInput({ value, lazy, onChange }: {
2
+ value: number;
3
+ lazy?: boolean;
4
+ onChange(value: number): void;
5
+ }): {
6
+ getInputProps(): Required<Pick<React.ComponentPropsWithoutRef<'input'>, 'value' | 'onChange' | 'onBlur'>>;
7
+ };
@@ -0,0 +1,49 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { isntNaN } from 'extra-utils';
3
+ export function useNumberInput({ value, lazy, onChange }) {
4
+ const [displayValue, setDisplayValue] = useState(() => value.toString());
5
+ useEffect(() => {
6
+ if (!lazy) {
7
+ const oldValue = Number(displayValue);
8
+ if (oldValue !== value)
9
+ setDisplayValue(value.toString());
10
+ }
11
+ }, [value, displayValue]);
12
+ useEffect(() => {
13
+ if (lazy) {
14
+ setDisplayValue(value.toString());
15
+ }
16
+ }, [value]);
17
+ return {
18
+ getInputProps() {
19
+ return {
20
+ value: displayValue,
21
+ onChange(e) {
22
+ setDisplayValue(e.target.value);
23
+ if (e.target.reportValidity()) {
24
+ if (!lazy) {
25
+ const newValue = e.target.valueAsNumber;
26
+ if (isntNaN(newValue) && newValue !== value) {
27
+ onChange(newValue);
28
+ }
29
+ }
30
+ }
31
+ },
32
+ onBlur(e) {
33
+ if (e.target.checkValidity()) {
34
+ if (lazy) {
35
+ const newValue = e.target.valueAsNumber;
36
+ if (isntNaN(newValue) && newValue !== value) {
37
+ onChange(newValue);
38
+ setDisplayValue(newValue.toString());
39
+ return;
40
+ }
41
+ }
42
+ }
43
+ setDisplayValue(value.toString());
44
+ }
45
+ };
46
+ }
47
+ };
48
+ }
49
+ //# sourceMappingURL=use-number-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-number-input.js","sourceRoot":"","sources":["../src/use-number-input.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAErC,MAAM,UAAU,cAAc,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAKrD;IAQC,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAS,GAAG,EAAE,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAA;IAEhF,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,QAAQ,GAAG,MAAM,CAAC,YAAY,CAAC,CAAA;YAGrC,IAAI,QAAQ,KAAK,KAAK;gBAAE,eAAe,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAA;QAC3D,CAAC;IACH,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC,CAAA;IAEzB,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,IAAI,EAAE,CAAC;YAGT,eAAe,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAA;QACnC,CAAC;IACH,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;IAEX,OAAO;QACL,aAAa;YACX,OAAO;gBACL,KAAK,EAAE,YAAY;gBACnB,QAAQ,CAAC,CAAC;oBACR,eAAe,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;oBAE/B,IAAI,CAAC,CAAC,MAAM,CAAC,cAAc,EAAE,EAAE,CAAC;wBAC9B,IAAI,CAAC,IAAI,EAAE,CAAC;4BACV,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC,aAAa,CAAA;4BAEvC,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;gCAC5C,QAAQ,CAAC,QAAQ,CAAC,CAAA;4BACpB,CAAC;wBACH,CAAC;oBACH,CAAC;gBACH,CAAC;gBACD,MAAM,CAAC,CAAC;oBACN,IAAI,CAAC,CAAC,MAAM,CAAC,aAAa,EAAE,EAAE,CAAC;wBAC7B,IAAI,IAAI,EAAE,CAAC;4BACT,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC,aAAa,CAAA;4BAEvC,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;gCAC5C,QAAQ,CAAC,QAAQ,CAAC,CAAA;gCAIlB,eAAe,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAA;gCAEpC,OAAM;4BACR,CAAC;wBACH,CAAC;oBACH,CAAC;oBAGD,eAAe,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAA;gBACnC,CAAC;aACF,CAAA;QACH,CAAC;KACF,CAAA;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "extra-react-hooks",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "files": [
@@ -40,6 +40,7 @@
40
40
  "@eslint/js": "^9.18.0",
41
41
  "@testing-library/dom": "^10.4.0",
42
42
  "@testing-library/react": "^16.1.0",
43
+ "@testing-library/user-event": "^14.6.1",
43
44
  "@types/jasmine": "^5.1.5",
44
45
  "@types/react": "^19.0.7",
45
46
  "eslint": "^9.18.0",
@@ -71,6 +72,7 @@
71
72
  "@blackglory/prelude": "^0.4.0",
72
73
  "extra-abort": "^0.4.0",
73
74
  "extra-fsm": "^0.2.1",
75
+ "extra-timers": "^0.2.6",
74
76
  "extra-utils": "^5.17.0",
75
77
  "rxjs": "^7.8.1"
76
78
  },
package/src/index.ts CHANGED
@@ -21,3 +21,5 @@ export * from './use-iife.js'
21
21
  export * from './use-memo-with-cleanup.js'
22
22
  export * from './use-get-set.js'
23
23
  export * from './use-isomorphic-layout-effect.js'
24
+ export * from './use-debounce.js'
25
+ export * from './use-number-input.js'
@@ -1,11 +1,11 @@
1
- import { useCallback, DependencyList } from 'react'
1
+ import { useCallback } from 'react'
2
2
  import { AbortController, raceAbortSignals } from 'extra-abort'
3
3
  import { useMemoWithCleanup } from './use-memo-with-cleanup.js'
4
4
  import { Falsy } from '@blackglory/prelude'
5
5
 
6
6
  export function useAbortableCallback<Args extends unknown[], Result>(
7
7
  callback: (...args: [...args: Args, signal: AbortSignal]) => PromiseLike<Result>
8
- , deps: DependencyList
8
+ , deps: React.DependencyList
9
9
  ): (...args: [...args: Args, signal: AbortSignal | Falsy]) => Promise<Result> {
10
10
  const controller = useMemoWithCleanup(
11
11
  () => new AbortController()
@@ -0,0 +1,25 @@
1
+ import { useRef, useEffect } from 'react'
2
+ import { setTimeout } from 'extra-timers'
3
+
4
+ export function useDebounce<Args extends unknown[]>(
5
+ fn: (...args: Args) => void
6
+ , timeout: number
7
+ , deps?: React.DependencyList
8
+ ): (...args: Args) => void {
9
+ const fnRef = useRef<(...args: Args) => void>(fn)
10
+ const cancelTimeoutRef = useRef<() => void>(undefined)
11
+
12
+ useEffect(() => {
13
+ fnRef.current = fn
14
+ }, deps)
15
+
16
+ return (...args: Args): void => {
17
+ cancelTimeoutRef.current?.()
18
+
19
+ cancelTimeoutRef.current = setTimeout(timeout, () => {
20
+ fnRef.current(...args)
21
+
22
+ cancelTimeoutRef.current = undefined
23
+ })
24
+ }
25
+ }
@@ -1,4 +1,4 @@
1
- import { useEffect, DependencyList } from 'react'
1
+ import { useEffect } from 'react'
2
2
  import { go, pass } from '@blackglory/prelude'
3
3
  import { AbortController } from 'extra-abort'
4
4
  import { CustomError } from '@blackglory/errors'
@@ -7,7 +7,7 @@ class InternalAbortError extends CustomError {}
7
7
 
8
8
  export function useEffectAsync(
9
9
  effect: (signal: AbortSignal) => Promise<void>
10
- , deps?: DependencyList
10
+ , deps?: React.DependencyList
11
11
  ): void {
12
12
  useEffect(() => {
13
13
  const controller = new AbortController()
@@ -0,0 +1,76 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { isntNaN } from 'extra-utils'
3
+
4
+ export function useNumberInput({ value, lazy, onChange }: {
5
+ value: number
6
+ lazy?: boolean
7
+
8
+ onChange(value: number): void
9
+ }): {
10
+ getInputProps(): Required<Pick<
11
+ React.ComponentPropsWithoutRef<'input'>
12
+ , 'value'
13
+ | 'onChange'
14
+ | 'onBlur'
15
+ >>
16
+ } {
17
+ const [displayValue, setDisplayValue] = useState<string>(() => value.toString())
18
+
19
+ useEffect(() => {
20
+ if (!lazy) {
21
+ const oldValue = Number(displayValue)
22
+
23
+ // 以传入的值为准, 但只有在实际代表的值不同时, 才更新.
24
+ if (oldValue !== value) setDisplayValue(value.toString())
25
+ }
26
+ }, [value, displayValue])
27
+
28
+ useEffect(() => {
29
+ if (lazy) {
30
+ // 以传入的值为准.
31
+ // 由于onChange是惰性调用, 不需要检查值的相等性.
32
+ setDisplayValue(value.toString())
33
+ }
34
+ }, [value])
35
+
36
+ return {
37
+ getInputProps() {
38
+ return {
39
+ value: displayValue
40
+ , onChange(e) {
41
+ setDisplayValue(e.target.value)
42
+
43
+ if (e.target.reportValidity()) {
44
+ if (!lazy) {
45
+ const newValue = e.target.valueAsNumber
46
+
47
+ if (isntNaN(newValue) && newValue !== value) {
48
+ onChange(newValue)
49
+ }
50
+ }
51
+ }
52
+ }
53
+ , onBlur(e) {
54
+ if (e.target.checkValidity()) {
55
+ if (lazy) {
56
+ const newValue = e.target.valueAsNumber
57
+
58
+ if (isntNaN(newValue) && newValue !== value) {
59
+ onChange(newValue)
60
+
61
+ // 使用格式化后的新值.
62
+ // 若hook的调用方决定不接受新值, 则displayValue的值会被覆盖.
63
+ setDisplayValue(newValue.toString())
64
+
65
+ return
66
+ }
67
+ }
68
+ }
69
+
70
+ // 继续使用旧值, 但对旧值格式化.
71
+ setDisplayValue(value.toString())
72
+ }
73
+ }
74
+ }
75
+ }
76
+ }