@rethunk/react-load-hooks 0.2.0 → 0.3.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 +6 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/loader.d.ts +4 -2
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +8 -1
- package/dist/useDebouncedLoad.d.ts +3 -2
- package/dist/useDebouncedLoad.d.ts.map +1 -1
- package/dist/useLoaded.d.ts +2 -1
- package/dist/useLoaded.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +1 -1
- package/src/loader.ts +14 -4
- package/src/useDebouncedLoad.ts +3 -3
- package/src/useLoaded.ts +2 -2
package/README.md
CHANGED
|
@@ -39,13 +39,13 @@ A worked example and the contracts: [HUMANS.md](HUMANS.md).
|
|
|
39
39
|
|
|
40
40
|
## Documentation
|
|
41
41
|
|
|
42
|
-
| Doc
|
|
43
|
-
|
|
|
44
|
-
| [HUMANS.md](HUMANS.md)
|
|
45
|
-
| [AGENTS.md](AGENTS.md)
|
|
42
|
+
| Doc | Audience |
|
|
43
|
+
| ---------------------------------- | ------------------------------ |
|
|
44
|
+
| [HUMANS.md](HUMANS.md) | Running and using the package |
|
|
45
|
+
| [AGENTS.md](AGENTS.md) | Internals, invariants, and why |
|
|
46
46
|
| [CONTRIBUTING.md](CONTRIBUTING.md) | Process — commits, tests, docs |
|
|
47
|
-
| [SECURITY.md](SECURITY.md)
|
|
48
|
-
| [CHANGELOG.md](CHANGELOG.md)
|
|
47
|
+
| [SECURITY.md](SECURITY.md) | Vulnerability disclosure |
|
|
48
|
+
| [CHANGELOG.md](CHANGELOG.md) | Release notes |
|
|
49
49
|
|
|
50
50
|
## License
|
|
51
51
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { coalescer } from './coalescer.ts';
|
|
2
|
-
export { type LoadSink, loader } from './loader.ts';
|
|
2
|
+
export { type Load, type LoadSink, loader } from './loader.ts';
|
|
3
3
|
export { keepFields, keepUnchanged, sameValue } from './sameValue.ts';
|
|
4
4
|
export { useDebouncedLoad } from './useDebouncedLoad.ts';
|
|
5
5
|
export { useDebouncedValue } from './useDebouncedValue.ts';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,QAAQ,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAC9D,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1D,OAAO,EAAE,KAAK,aAAa,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC9D,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACzD,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAC7E,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA"}
|
package/dist/loader.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export interface LoadSink<T> {
|
|
|
6
6
|
/** Shows the hook's initial value again. */
|
|
7
7
|
reset: () => void;
|
|
8
8
|
}
|
|
9
|
+
/** `signal` aborts when a later load, new deps or stop supersedes this one: pass it to fetch to stop the abandoned work. */
|
|
10
|
+
export type Load<T> = (signal: AbortSignal) => Promise<T>;
|
|
9
11
|
/**
|
|
10
12
|
* useLoaded's and useDebouncedLoad's logic without React. Only the newest load lands: a later load, new deps or stop
|
|
11
13
|
* drops the answer, error and end of every one before it. New deps reset the value unless keepAcrossKeys, and a null
|
|
@@ -15,9 +17,9 @@ export declare function loader<T>(sink: LoadSink<T>, { keepAcrossKeys, defer, }?
|
|
|
15
17
|
keepAcrossKeys?: boolean;
|
|
16
18
|
defer?: (go: () => void) => () => void;
|
|
17
19
|
}): {
|
|
18
|
-
deps(deps: DependencyList, load:
|
|
20
|
+
deps(deps: DependencyList, load: Load<T> | null): void;
|
|
19
21
|
/** Runs a load at once with the same deps, keeping the value shown until it answers; resolves when it has. */
|
|
20
|
-
run(load:
|
|
22
|
+
run(load: Load<T> | null): Promise<void>;
|
|
21
23
|
stop(): void;
|
|
22
24
|
};
|
|
23
25
|
//# sourceMappingURL=loader.d.ts.map
|
package/dist/loader.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAK3C,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAA;IACzB,KAAK,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAC/B,OAAO,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,IAAI,CAAA;IAC9B,4CAA4C;IAC5C,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAK3C,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAA;IACzB,KAAK,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAC/B,OAAO,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,IAAI,CAAA;IAC9B,4CAA4C;IAC5C,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAED,4HAA4H;AAC5H,MAAM,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,CAAA;AAOzD;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,CAAC,EACtB,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,EACjB,EACE,cAAsB,EACtB,KAAW,GACZ,GAAE;IAAE,cAAc,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAA;CAAO;IA8C1E,IAAI,OAAO,cAAc,QAAQ,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI;IAoB/C,8GAA8G;IAC9G,GAAG,OAAO,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAOxC,IAAI;EAKP"}
|
package/dist/loader.js
CHANGED
|
@@ -12,16 +12,23 @@ export function loader(sink, { keepAcrossKeys = false, defer = now, } = {}) {
|
|
|
12
12
|
let generation = 0;
|
|
13
13
|
let seen = null;
|
|
14
14
|
let cancel = () => undefined;
|
|
15
|
+
let controller = null;
|
|
15
16
|
const drop = () => {
|
|
16
17
|
generation += 1;
|
|
17
18
|
cancel();
|
|
19
|
+
controller?.abort();
|
|
20
|
+
controller = null;
|
|
18
21
|
};
|
|
19
22
|
const start = (load) => {
|
|
20
23
|
generation += 1;
|
|
24
|
+
controller?.abort();
|
|
21
25
|
const mine = generation;
|
|
22
26
|
const current = () => generation === mine;
|
|
27
|
+
const mineController = new AbortController();
|
|
28
|
+
controller = mineController;
|
|
23
29
|
sink.loading(true);
|
|
24
|
-
|
|
30
|
+
// A load that throws before returning a promise settles through the sink like a rejection.
|
|
31
|
+
return new Promise((resolve) => resolve(load(mineController.signal)))
|
|
25
32
|
.then((value) => {
|
|
26
33
|
if (current()) {
|
|
27
34
|
sink.value(value);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { type DependencyList } from 'react';
|
|
2
|
+
import { type Load } from './loader.ts';
|
|
2
3
|
/**
|
|
3
4
|
* useLoaded for input that changes on every key: `load` runs `delayMs` (read afresh each time a load is scheduled)
|
|
4
5
|
* after `deps` last changed, and `data` keeps the last answer meanwhile. Only the newest run lands, so `onError` hears
|
|
5
6
|
* only the newest failure. A null `load` resets `data` to `initial`. `run` starts another load at once, such as the
|
|
6
7
|
* next page, under the same rule.
|
|
7
8
|
*/
|
|
8
|
-
export declare function useDebouncedLoad<T>(load:
|
|
9
|
+
export declare function useDebouncedLoad<T>(load: Load<T> | null, deps: DependencyList, { initial, delayMs, onError, }: {
|
|
9
10
|
initial: T;
|
|
10
11
|
delayMs: number;
|
|
11
12
|
onError?: (error: unknown) => void;
|
|
@@ -13,6 +14,6 @@ export declare function useDebouncedLoad<T>(load: (() => Promise<T>) | null, dep
|
|
|
13
14
|
data: T;
|
|
14
15
|
loading: boolean;
|
|
15
16
|
error: unknown;
|
|
16
|
-
run: (next:
|
|
17
|
+
run: (next: Load<T>) => Promise<void>;
|
|
17
18
|
};
|
|
18
19
|
//# sourceMappingURL=useDebouncedLoad.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDebouncedLoad.d.ts","sourceRoot":"","sources":["../src/useDebouncedLoad.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAA+B,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"useDebouncedLoad.d.ts","sourceRoot":"","sources":["../src/useDebouncedLoad.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAA+B,MAAM,OAAO,CAAA;AACxE,OAAO,EAAE,KAAK,IAAI,EAAU,MAAM,aAAa,CAAA;AAE/C;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,EACpB,IAAI,EAAE,cAAc,EACpB,EACE,OAAO,EACP,OAAO,EACP,OAAO,GACR,EAAE;IAAE,OAAO,EAAE,CAAC,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE;;;;gBAwCxD,IAAI,CAAC,CAAC,CAAC;EAEtB"}
|
package/dist/useLoaded.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type DependencyList } from 'react';
|
|
2
|
+
import { type Load } from './loader.ts';
|
|
2
3
|
export interface LoadedOptions {
|
|
3
4
|
onError?: (error: unknown) => void;
|
|
4
5
|
/** Keep `data` across new deps until the new answer lands. */
|
|
@@ -11,7 +12,7 @@ export interface LoadedOptions {
|
|
|
11
12
|
* `setData` edits the loaded value in place. The fourth argument is `onError` or `{ onError, keepAcrossKeys }`. With `keepAcrossKeys`, new deps keep `data` until the new answer lands
|
|
12
13
|
* (a null load still resets it).
|
|
13
14
|
*/
|
|
14
|
-
export declare function useLoaded<T>(load:
|
|
15
|
+
export declare function useLoaded<T>(load: Load<T> | null, deps: DependencyList, initial: T, options?: ((error: unknown) => void) | LoadedOptions): {
|
|
15
16
|
data: T;
|
|
16
17
|
setData: import("react").Dispatch<import("react").SetStateAction<T>>;
|
|
17
18
|
loading: boolean;
|
package/dist/useLoaded.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLoaded.d.ts","sourceRoot":"","sources":["../src/useLoaded.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAA4C,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"useLoaded.d.ts","sourceRoot":"","sources":["../src/useLoaded.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAA4C,MAAM,OAAO,CAAA;AACrF,OAAO,EAAE,KAAK,IAAI,EAAU,MAAM,aAAa,CAAA;AAE/C,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAClC,8DAA8D;IAC9D,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,CAAC,EACzB,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,EACpB,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE,CAAC,EACV,OAAO,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,aAAa;;;;;;EAoCrD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rethunk/react-load-hooks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "React hooks for async loads that never land out of order, plus the small state hooks an app builds screens from.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Rethunk.Tech, LLC",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"pack:check": "bun scripts/check-pack.ts"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@biomejs/biome": "^2.6.
|
|
32
|
+
"@biomejs/biome": "^2.6.6",
|
|
33
33
|
"@happy-dom/global-registrator": "^21.0.4",
|
|
34
34
|
"@testing-library/dom": "^10.4.2",
|
|
35
35
|
"@testing-library/react": "^16.3.3",
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { coalescer } from './coalescer.ts'
|
|
2
|
-
export { type LoadSink, loader } from './loader.ts'
|
|
2
|
+
export { type Load, type LoadSink, loader } from './loader.ts'
|
|
3
3
|
export { keepFields, keepUnchanged, sameValue } from './sameValue.ts'
|
|
4
4
|
export { useDebouncedLoad } from './useDebouncedLoad.ts'
|
|
5
5
|
export { useDebouncedValue } from './useDebouncedValue.ts'
|
package/src/loader.ts
CHANGED
|
@@ -11,6 +11,9 @@ export interface LoadSink<T> {
|
|
|
11
11
|
reset: () => void
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/** `signal` aborts when a later load, new deps or stop supersedes this one: pass it to fetch to stop the abandoned work. */
|
|
15
|
+
export type Load<T> = (signal: AbortSignal) => Promise<T>
|
|
16
|
+
|
|
14
17
|
const now = (go: () => void) => {
|
|
15
18
|
go()
|
|
16
19
|
return () => undefined
|
|
@@ -31,16 +34,23 @@ export function loader<T>(
|
|
|
31
34
|
let generation = 0
|
|
32
35
|
let seen: DependencyList | null = null
|
|
33
36
|
let cancel: () => void = () => undefined
|
|
37
|
+
let controller: AbortController | null = null
|
|
34
38
|
const drop = () => {
|
|
35
39
|
generation += 1
|
|
36
40
|
cancel()
|
|
41
|
+
controller?.abort()
|
|
42
|
+
controller = null
|
|
37
43
|
}
|
|
38
|
-
const start = (load:
|
|
44
|
+
const start = (load: Load<T>): Promise<void> => {
|
|
39
45
|
generation += 1
|
|
46
|
+
controller?.abort()
|
|
40
47
|
const mine = generation
|
|
41
48
|
const current = () => generation === mine
|
|
49
|
+
const mineController = new AbortController()
|
|
50
|
+
controller = mineController
|
|
42
51
|
sink.loading(true)
|
|
43
|
-
|
|
52
|
+
// A load that throws before returning a promise settles through the sink like a rejection.
|
|
53
|
+
return new Promise<T>((resolve) => resolve(load(mineController.signal)))
|
|
44
54
|
.then(
|
|
45
55
|
(value) => {
|
|
46
56
|
if (current()) {
|
|
@@ -65,7 +75,7 @@ export function loader<T>(
|
|
|
65
75
|
sink.loading(false)
|
|
66
76
|
}
|
|
67
77
|
return {
|
|
68
|
-
deps(deps: DependencyList, load:
|
|
78
|
+
deps(deps: DependencyList, load: Load<T> | null) {
|
|
69
79
|
if (sameDeps(seen, deps)) {
|
|
70
80
|
return
|
|
71
81
|
}
|
|
@@ -86,7 +96,7 @@ export function loader<T>(
|
|
|
86
96
|
})
|
|
87
97
|
},
|
|
88
98
|
/** Runs a load at once with the same deps, keeping the value shown until it answers; resolves when it has. */
|
|
89
|
-
run(load:
|
|
99
|
+
run(load: Load<T> | null): Promise<void> {
|
|
90
100
|
if (load === null) {
|
|
91
101
|
skip()
|
|
92
102
|
return Promise.resolve()
|
package/src/useDebouncedLoad.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type DependencyList, useEffect, useRef, useState } from 'react'
|
|
2
|
-
import { loader } from './loader.ts'
|
|
2
|
+
import { type Load, loader } from './loader.ts'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* useLoaded for input that changes on every key: `load` runs `delayMs` (read afresh each time a load is scheduled)
|
|
@@ -8,7 +8,7 @@ import { loader } from './loader.ts'
|
|
|
8
8
|
* next page, under the same rule.
|
|
9
9
|
*/
|
|
10
10
|
export function useDebouncedLoad<T>(
|
|
11
|
-
load:
|
|
11
|
+
load: Load<T> | null,
|
|
12
12
|
deps: DependencyList,
|
|
13
13
|
{
|
|
14
14
|
initial,
|
|
@@ -54,6 +54,6 @@ export function useDebouncedLoad<T>(
|
|
|
54
54
|
data,
|
|
55
55
|
loading,
|
|
56
56
|
error,
|
|
57
|
-
run: (next:
|
|
57
|
+
run: (next: Load<T>) => runs.run(next),
|
|
58
58
|
}
|
|
59
59
|
}
|
package/src/useLoaded.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type DependencyList, useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
-
import { loader } from './loader.ts'
|
|
2
|
+
import { type Load, loader } from './loader.ts'
|
|
3
3
|
|
|
4
4
|
export interface LoadedOptions {
|
|
5
5
|
onError?: (error: unknown) => void
|
|
@@ -15,7 +15,7 @@ export interface LoadedOptions {
|
|
|
15
15
|
* (a null load still resets it).
|
|
16
16
|
*/
|
|
17
17
|
export function useLoaded<T>(
|
|
18
|
-
load:
|
|
18
|
+
load: Load<T> | null,
|
|
19
19
|
deps: DependencyList,
|
|
20
20
|
initial: T,
|
|
21
21
|
options?: ((error: unknown) => void) | LoadedOptions,
|