@rethunk/react-load-hooks 0.3.0 → 0.4.0

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
@@ -32,20 +32,20 @@ A worked example and the contracts: [HUMANS.md](HUMANS.md).
32
32
  - `useLoaded` and `useDebouncedLoad` fetch on dependency changes and drop superseded answers.
33
33
  - `loader` is the same logic without React, for a store or a class.
34
34
  - `useStoredState` persists UI state in `localStorage`.
35
- - `useWidth` tracks an element's width before paint; `useDebouncedValue` settles a fast-changing value.
35
+ - `useSize` tracks an element's width and height before paint; `useWidth` is the width only. `useDebouncedValue` settles a fast-changing value.
36
36
  - `sameValue`, `keepUnchanged` and `keepFields` keep the stored value when a fresh backend answer is equal, so subscribers do not re-render.
37
37
  - `clockWhen`, `absoluteWhen` and `relativeWhen` label a time through `Intl`: "3 minutes ago" within a week, a date after.
38
38
  - `useNowValue` and `useSecondValue` share one timer per period and re-render only on a changed answer.
39
39
 
40
40
  ## Documentation
41
41
 
42
- | Doc | Audience |
43
- | --- | --- |
44
- | [HUMANS.md](HUMANS.md) | Running and using the package |
45
- | [AGENTS.md](AGENTS.md) | Internals, invariants, and why |
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) | Vulnerability disclosure |
48
- | [CHANGELOG.md](CHANGELOG.md) | Release notes |
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
@@ -5,6 +5,7 @@ export { useDebouncedLoad } from './useDebouncedLoad.ts';
5
5
  export { useDebouncedValue } from './useDebouncedValue.ts';
6
6
  export { type LoadedOptions, useLoaded } from './useLoaded.ts';
7
7
  export { useNowValue, useSecondValue } from './useNow.ts';
8
+ export { useSize } from './useSize.ts';
8
9
  export { readStored, useStoredState, writeStored } from './useStoredState.ts';
9
10
  export { useWidth } from './useWidth.ts';
10
11
  export { absoluteWhen, clockWhen, relativeWhen, type WhenOptions } from './when.ts';
@@ -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,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"}
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,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,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/index.js CHANGED
@@ -5,6 +5,7 @@ export { useDebouncedLoad } from './useDebouncedLoad.js';
5
5
  export { useDebouncedValue } from './useDebouncedValue.js';
6
6
  export { useLoaded } from './useLoaded.js';
7
7
  export { useNowValue, useSecondValue } from './useNow.js';
8
+ export { useSize } from './useSize.js';
8
9
  export { readStored, useStoredState, writeStored } from './useStoredState.js';
9
10
  export { useWidth } from './useWidth.js';
10
11
  export { absoluteWhen, clockWhen, relativeWhen } from './when.js';
@@ -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;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;IA6C1E,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"}
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
@@ -27,7 +27,8 @@ export function loader(sink, { keepAcrossKeys = false, defer = now, } = {}) {
27
27
  const mineController = new AbortController();
28
28
  controller = mineController;
29
29
  sink.loading(true);
30
- return load(mineController.signal)
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)))
31
32
  .then((value) => {
32
33
  if (current()) {
33
34
  sink.value(value);
@@ -0,0 +1,8 @@
1
+ import { type RefObject } from 'react';
2
+ /** The client size of the element the ref points at, kept current as it resizes; 0 until measured. */
3
+ declare function useSize<T extends HTMLElement>(ref: RefObject<T | null>): {
4
+ width: number;
5
+ height: number;
6
+ };
7
+ export { useSize };
8
+ //# sourceMappingURL=useSize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSize.d.ts","sourceRoot":"","sources":["../src/useSize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAA6B,MAAM,OAAO,CAAA;AAGjE,sGAAsG;AACtG,iBAAS,OAAO,CAAC,CAAC,SAAS,WAAW,EACpC,GAAG,EAAE,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,GACvB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAsBnC;AAED,OAAO,EAAE,OAAO,EAAE,CAAA"}
@@ -0,0 +1,24 @@
1
+ import { useLayoutEffect, useState } from 'react';
2
+ import { flushSync } from 'react-dom';
3
+ /** The client size of the element the ref points at, kept current as it resizes; 0 until measured. */
4
+ function useSize(ref) {
5
+ const [size, setSize] = useState({ width: 0, height: 0 });
6
+ // Measured before paint, so the first frame is laid out for the real size rather than 0.
7
+ useLayoutEffect(() => {
8
+ const el = ref.current;
9
+ if (!el) {
10
+ return;
11
+ }
12
+ const apply = () => setSize((prev) => prev.width === el.clientWidth && prev.height === el.clientHeight
13
+ ? prev
14
+ : { width: el.clientWidth, height: el.clientHeight });
15
+ // Rendered before the browser paints the resize: a default-priority update would paint one frame laid out for the
16
+ // old size, with whatever columns or scrollbar that size implies.
17
+ const ro = new ResizeObserver(() => flushSync(apply));
18
+ ro.observe(el);
19
+ apply();
20
+ return () => ro.disconnect();
21
+ }, [ref]);
22
+ return size;
23
+ }
24
+ export { useSize };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rethunk/react-load-hooks",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
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.4",
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
@@ -5,6 +5,7 @@ export { useDebouncedLoad } from './useDebouncedLoad.ts'
5
5
  export { useDebouncedValue } from './useDebouncedValue.ts'
6
6
  export { type LoadedOptions, useLoaded } from './useLoaded.ts'
7
7
  export { useNowValue, useSecondValue } from './useNow.ts'
8
+ export { useSize } from './useSize.ts'
8
9
  export { readStored, useStoredState, writeStored } from './useStoredState.ts'
9
10
  export { useWidth } from './useWidth.ts'
10
11
  export { absoluteWhen, clockWhen, relativeWhen, type WhenOptions } from './when.ts'
package/src/loader.ts CHANGED
@@ -49,7 +49,8 @@ export function loader<T>(
49
49
  const mineController = new AbortController()
50
50
  controller = mineController
51
51
  sink.loading(true)
52
- return load(mineController.signal)
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)))
53
54
  .then(
54
55
  (value) => {
55
56
  if (current()) {
package/src/useSize.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { type RefObject, useLayoutEffect, useState } from 'react'
2
+ import { flushSync } from 'react-dom'
3
+
4
+ /** The client size of the element the ref points at, kept current as it resizes; 0 until measured. */
5
+ function useSize<T extends HTMLElement>(
6
+ ref: RefObject<T | null>,
7
+ ): { width: number; height: number } {
8
+ const [size, setSize] = useState({ width: 0, height: 0 })
9
+ // Measured before paint, so the first frame is laid out for the real size rather than 0.
10
+ useLayoutEffect(() => {
11
+ const el = ref.current
12
+ if (!el) {
13
+ return
14
+ }
15
+ const apply = () =>
16
+ setSize((prev) =>
17
+ prev.width === el.clientWidth && prev.height === el.clientHeight
18
+ ? prev
19
+ : { width: el.clientWidth, height: el.clientHeight },
20
+ )
21
+ // Rendered before the browser paints the resize: a default-priority update would paint one frame laid out for the
22
+ // old size, with whatever columns or scrollbar that size implies.
23
+ const ro = new ResizeObserver(() => flushSync(apply))
24
+ ro.observe(el)
25
+ apply()
26
+ return () => ro.disconnect()
27
+ }, [ref])
28
+ return size
29
+ }
30
+
31
+ export { useSize }