@rethunk/react-load-hooks 0.1.1 → 0.2.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 +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/sameValue.d.ts +7 -0
- package/dist/sameValue.d.ts.map +1 -0
- package/dist/sameValue.js +27 -0
- package/dist/when.d.ts +11 -0
- package/dist/when.d.ts.map +1 -0
- package/dist/when.js +60 -0
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/sameValue.ts +30 -0
- package/src/when.ts +82 -0
package/README.md
CHANGED
|
@@ -33,6 +33,8 @@ A worked example and the contracts: [HUMANS.md](HUMANS.md).
|
|
|
33
33
|
- `loader` is the same logic without React, for a store or a class.
|
|
34
34
|
- `useStoredState` persists UI state in `localStorage`.
|
|
35
35
|
- `useWidth` tracks an element's width before paint; `useDebouncedValue` settles a fast-changing value.
|
|
36
|
+
- `sameValue`, `keepUnchanged` and `keepFields` keep the stored value when a fresh backend answer is equal, so subscribers do not re-render.
|
|
37
|
+
- `clockWhen`, `absoluteWhen` and `relativeWhen` label a time through `Intl`: "3 minutes ago" within a week, a date after.
|
|
36
38
|
- `useNowValue` and `useSecondValue` share one timer per period and re-render only on a changed answer.
|
|
37
39
|
|
|
38
40
|
## Documentation
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export { coalescer } from './coalescer.ts';
|
|
2
2
|
export { type LoadSink, loader } from './loader.ts';
|
|
3
|
+
export { keepFields, keepUnchanged, sameValue } from './sameValue.ts';
|
|
3
4
|
export { useDebouncedLoad } from './useDebouncedLoad.ts';
|
|
4
5
|
export { useDebouncedValue } from './useDebouncedValue.ts';
|
|
5
6
|
export { type LoadedOptions, useLoaded } from './useLoaded.ts';
|
|
6
7
|
export { useNowValue, useSecondValue } from './useNow.ts';
|
|
7
8
|
export { readStored, useStoredState, writeStored } from './useStoredState.ts';
|
|
8
9
|
export { useWidth } from './useWidth.ts';
|
|
10
|
+
export { absoluteWhen, clockWhen, relativeWhen, type WhenOptions } from './when.ts';
|
|
9
11
|
//# sourceMappingURL=index.d.ts.map
|
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;AACnD,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"}
|
|
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;AACnD,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/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export { coalescer } from './coalescer.js';
|
|
2
2
|
export { loader } from './loader.js';
|
|
3
|
+
export { keepFields, keepUnchanged, sameValue } from './sameValue.js';
|
|
3
4
|
export { useDebouncedLoad } from './useDebouncedLoad.js';
|
|
4
5
|
export { useDebouncedValue } from './useDebouncedValue.js';
|
|
5
6
|
export { useLoaded } from './useLoaded.js';
|
|
6
7
|
export { useNowValue, useSecondValue } from './useNow.js';
|
|
7
8
|
export { readStored, useStoredState, writeStored } from './useStoredState.js';
|
|
8
9
|
export { useWidth } from './useWidth.js';
|
|
10
|
+
export { absoluteWhen, clockWhen, relativeWhen } from './when.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function sameValue(a: unknown, b: unknown): boolean;
|
|
2
|
+
/** next, with each item equal to the previous item of the same key replaced by that previous object. */
|
|
3
|
+
export declare function keepUnchanged<T>(prev: readonly T[], next: T[], keyOf: (item: T) => string): T[];
|
|
4
|
+
/** next, with each field equal to the same field of prev kept as prev's value, so a selector of an unchanged field
|
|
5
|
+
* keeps returning the same object. */
|
|
6
|
+
export declare function keepFields<T extends object>(prev: T, next: T): T;
|
|
7
|
+
//# sourceMappingURL=sameValue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sameValue.d.ts","sourceRoot":"","sources":["../src/sameValue.ts"],"names":[],"mappings":"AAGA,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAEzD;AAED,wGAAwG;AACxG,wBAAgB,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,CAAC,EAAE,CAS/F;AAED;sCACsC;AACtC,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CAQhE"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Backend answers arrive as fresh objects even when nothing changed; storing one as-is re-renders every subscriber
|
|
2
|
+
// and defeats memoised rows. These keep the stored value whenever the answer is equal to it.
|
|
3
|
+
export function sameValue(a, b) {
|
|
4
|
+
return a === b || JSON.stringify(a) === JSON.stringify(b);
|
|
5
|
+
}
|
|
6
|
+
/** next, with each item equal to the previous item of the same key replaced by that previous object. */
|
|
7
|
+
export function keepUnchanged(prev, next, keyOf) {
|
|
8
|
+
const old = new Map(prev.map((item) => [keyOf(item), item]));
|
|
9
|
+
const kept = next.map((item) => {
|
|
10
|
+
const was = old.get(keyOf(item));
|
|
11
|
+
return was !== undefined && sameValue(was, item) ? was : item;
|
|
12
|
+
});
|
|
13
|
+
return kept.length === prev.length && kept.every((item, i) => item === prev[i])
|
|
14
|
+
? prev
|
|
15
|
+
: kept;
|
|
16
|
+
}
|
|
17
|
+
/** next, with each field equal to the same field of prev kept as prev's value, so a selector of an unchanged field
|
|
18
|
+
* keeps returning the same object. */
|
|
19
|
+
export function keepFields(prev, next) {
|
|
20
|
+
const out = { ...next };
|
|
21
|
+
for (const k of Object.keys(next)) {
|
|
22
|
+
if (sameValue(prev[k], next[k])) {
|
|
23
|
+
out[k] = prev[k];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
package/dist/when.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface WhenOptions {
|
|
2
|
+
withTime?: boolean;
|
|
3
|
+
now?: number;
|
|
4
|
+
locale?: string;
|
|
5
|
+
fewSeconds?: string;
|
|
6
|
+
absolute?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function clockWhen(value: string | number | Date, locale?: string): string;
|
|
9
|
+
export declare function absoluteWhen(value: string | number | Date, locale?: string): string;
|
|
10
|
+
export declare function relativeWhen(value: string | number | Date, { withTime, now, locale, fewSeconds, absolute, }?: WhenOptions): string;
|
|
11
|
+
//# sourceMappingURL=when.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"when.d.ts","sourceRoot":"","sources":["../src/when.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAGD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,EAAE,MAAM,SAAO,GAAG,MAAM,CAG9E;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,EAAE,MAAM,SAAO,GAAG,MAAM,CASjF;AAID,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,EAC7B,EACE,QAAgB,EAChB,GAAgB,EAChB,MAAa,EACb,UAAgC,EAChC,QAAgB,GACjB,GAAE,WAAgB,GAClB,MAAM,CA6BR"}
|
package/dist/when.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const MINUTE = 60_000;
|
|
2
|
+
const HOUR = 60 * MINUTE;
|
|
3
|
+
const DAY = 24 * HOUR;
|
|
4
|
+
const DAYS_PER_WEEK = 7;
|
|
5
|
+
const WEEK = DAYS_PER_WEEK * DAY;
|
|
6
|
+
const FIRST_YEAR = 1970;
|
|
7
|
+
function parseWhen(value) {
|
|
8
|
+
const d = new Date(value);
|
|
9
|
+
if (Number.isNaN(d.getTime()) || d.getUTCFullYear() < FIRST_YEAR) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
return d;
|
|
13
|
+
}
|
|
14
|
+
// clockWhen is the time of day as the locale writes it ("9:05 AM", "09:05"), '' for a zero or unparseable time.
|
|
15
|
+
export function clockWhen(value, locale = 'en') {
|
|
16
|
+
const d = parseWhen(value);
|
|
17
|
+
return d ? new Intl.DateTimeFormat(locale, { timeStyle: 'short' }).format(d) : '';
|
|
18
|
+
}
|
|
19
|
+
export function absoluteWhen(value, locale = 'en') {
|
|
20
|
+
const d = parseWhen(value);
|
|
21
|
+
if (!d) {
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
return new Intl.DateTimeFormat(locale, {
|
|
25
|
+
dateStyle: 'medium',
|
|
26
|
+
timeStyle: 'short',
|
|
27
|
+
}).format(d);
|
|
28
|
+
}
|
|
29
|
+
// relativeWhen reads a moment as "3 minutes ago" or "yesterday" within the last week and as a date before that,
|
|
30
|
+
// with the time too when withTime is set. A zero or unparseable time gives ''.
|
|
31
|
+
export function relativeWhen(value, { withTime = false, now = Date.now(), locale = 'en', fewSeconds = 'a few seconds ago', absolute = false, } = {}) {
|
|
32
|
+
const d = parseWhen(value);
|
|
33
|
+
if (!d) {
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
if (absolute) {
|
|
37
|
+
return new Intl.DateTimeFormat(locale, {
|
|
38
|
+
dateStyle: 'medium',
|
|
39
|
+
...(withTime ? { timeStyle: 'short' } : {}),
|
|
40
|
+
}).format(d);
|
|
41
|
+
}
|
|
42
|
+
const elapsed = now - d.getTime();
|
|
43
|
+
if (elapsed > -MINUTE && elapsed < MINUTE) {
|
|
44
|
+
return fewSeconds;
|
|
45
|
+
}
|
|
46
|
+
if (elapsed >= MINUTE && elapsed < WEEK) {
|
|
47
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
|
|
48
|
+
if (elapsed < HOUR) {
|
|
49
|
+
return rtf.format(-Math.floor(elapsed / MINUTE), 'minute');
|
|
50
|
+
}
|
|
51
|
+
if (elapsed < DAY) {
|
|
52
|
+
return rtf.format(-Math.floor(elapsed / HOUR), 'hour');
|
|
53
|
+
}
|
|
54
|
+
return rtf.format(-Math.floor(elapsed / DAY), 'day');
|
|
55
|
+
}
|
|
56
|
+
return new Intl.DateTimeFormat(locale, {
|
|
57
|
+
dateStyle: 'medium',
|
|
58
|
+
...(withTime ? { timeStyle: 'short' } : {}),
|
|
59
|
+
}).format(d);
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rethunk/react-load-hooks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export { coalescer } from './coalescer.ts'
|
|
2
2
|
export { type LoadSink, loader } from './loader.ts'
|
|
3
|
+
export { keepFields, keepUnchanged, sameValue } from './sameValue.ts'
|
|
3
4
|
export { useDebouncedLoad } from './useDebouncedLoad.ts'
|
|
4
5
|
export { useDebouncedValue } from './useDebouncedValue.ts'
|
|
5
6
|
export { type LoadedOptions, useLoaded } from './useLoaded.ts'
|
|
6
7
|
export { useNowValue, useSecondValue } from './useNow.ts'
|
|
7
8
|
export { readStored, useStoredState, writeStored } from './useStoredState.ts'
|
|
8
9
|
export { useWidth } from './useWidth.ts'
|
|
10
|
+
export { absoluteWhen, clockWhen, relativeWhen, type WhenOptions } from './when.ts'
|
package/src/sameValue.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Backend answers arrive as fresh objects even when nothing changed; storing one as-is re-renders every subscriber
|
|
2
|
+
// and defeats memoised rows. These keep the stored value whenever the answer is equal to it.
|
|
3
|
+
|
|
4
|
+
export function sameValue(a: unknown, b: unknown): boolean {
|
|
5
|
+
return a === b || JSON.stringify(a) === JSON.stringify(b)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** next, with each item equal to the previous item of the same key replaced by that previous object. */
|
|
9
|
+
export function keepUnchanged<T>(prev: readonly T[], next: T[], keyOf: (item: T) => string): T[] {
|
|
10
|
+
const old = new Map(prev.map((item) => [keyOf(item), item]))
|
|
11
|
+
const kept = next.map((item) => {
|
|
12
|
+
const was = old.get(keyOf(item))
|
|
13
|
+
return was !== undefined && sameValue(was, item) ? was : item
|
|
14
|
+
})
|
|
15
|
+
return kept.length === prev.length && kept.every((item, i) => item === prev[i])
|
|
16
|
+
? (prev as T[])
|
|
17
|
+
: kept
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** next, with each field equal to the same field of prev kept as prev's value, so a selector of an unchanged field
|
|
21
|
+
* keeps returning the same object. */
|
|
22
|
+
export function keepFields<T extends object>(prev: T, next: T): T {
|
|
23
|
+
const out = { ...next }
|
|
24
|
+
for (const k of Object.keys(next) as (keyof T)[]) {
|
|
25
|
+
if (sameValue(prev[k], next[k])) {
|
|
26
|
+
out[k] = prev[k]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return out
|
|
30
|
+
}
|
package/src/when.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const MINUTE = 60_000
|
|
2
|
+
const HOUR = 60 * MINUTE
|
|
3
|
+
const DAY = 24 * HOUR
|
|
4
|
+
const DAYS_PER_WEEK = 7
|
|
5
|
+
const WEEK = DAYS_PER_WEEK * DAY
|
|
6
|
+
const FIRST_YEAR = 1970
|
|
7
|
+
|
|
8
|
+
function parseWhen(value: string | number | Date): Date | undefined {
|
|
9
|
+
const d = new Date(value)
|
|
10
|
+
if (Number.isNaN(d.getTime()) || d.getUTCFullYear() < FIRST_YEAR) {
|
|
11
|
+
return undefined
|
|
12
|
+
}
|
|
13
|
+
return d
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface WhenOptions {
|
|
17
|
+
withTime?: boolean
|
|
18
|
+
now?: number
|
|
19
|
+
locale?: string
|
|
20
|
+
// The translated text for under a minute ago, which Intl words as "now".
|
|
21
|
+
fewSeconds?: string
|
|
22
|
+
absolute?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// clockWhen is the time of day as the locale writes it ("9:05 AM", "09:05"), '' for a zero or unparseable time.
|
|
26
|
+
export function clockWhen(value: string | number | Date, locale = 'en'): string {
|
|
27
|
+
const d = parseWhen(value)
|
|
28
|
+
return d ? new Intl.DateTimeFormat(locale, { timeStyle: 'short' }).format(d) : ''
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function absoluteWhen(value: string | number | Date, locale = 'en'): string {
|
|
32
|
+
const d = parseWhen(value)
|
|
33
|
+
if (!d) {
|
|
34
|
+
return ''
|
|
35
|
+
}
|
|
36
|
+
return new Intl.DateTimeFormat(locale, {
|
|
37
|
+
dateStyle: 'medium',
|
|
38
|
+
timeStyle: 'short',
|
|
39
|
+
}).format(d)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// relativeWhen reads a moment as "3 minutes ago" or "yesterday" within the last week and as a date before that,
|
|
43
|
+
// with the time too when withTime is set. A zero or unparseable time gives ''.
|
|
44
|
+
export function relativeWhen(
|
|
45
|
+
value: string | number | Date,
|
|
46
|
+
{
|
|
47
|
+
withTime = false,
|
|
48
|
+
now = Date.now(),
|
|
49
|
+
locale = 'en',
|
|
50
|
+
fewSeconds = 'a few seconds ago',
|
|
51
|
+
absolute = false,
|
|
52
|
+
}: WhenOptions = {},
|
|
53
|
+
): string {
|
|
54
|
+
const d = parseWhen(value)
|
|
55
|
+
if (!d) {
|
|
56
|
+
return ''
|
|
57
|
+
}
|
|
58
|
+
if (absolute) {
|
|
59
|
+
return new Intl.DateTimeFormat(locale, {
|
|
60
|
+
dateStyle: 'medium',
|
|
61
|
+
...(withTime ? { timeStyle: 'short' as const } : {}),
|
|
62
|
+
}).format(d)
|
|
63
|
+
}
|
|
64
|
+
const elapsed = now - d.getTime()
|
|
65
|
+
if (elapsed > -MINUTE && elapsed < MINUTE) {
|
|
66
|
+
return fewSeconds
|
|
67
|
+
}
|
|
68
|
+
if (elapsed >= MINUTE && elapsed < WEEK) {
|
|
69
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' })
|
|
70
|
+
if (elapsed < HOUR) {
|
|
71
|
+
return rtf.format(-Math.floor(elapsed / MINUTE), 'minute')
|
|
72
|
+
}
|
|
73
|
+
if (elapsed < DAY) {
|
|
74
|
+
return rtf.format(-Math.floor(elapsed / HOUR), 'hour')
|
|
75
|
+
}
|
|
76
|
+
return rtf.format(-Math.floor(elapsed / DAY), 'day')
|
|
77
|
+
}
|
|
78
|
+
return new Intl.DateTimeFormat(locale, {
|
|
79
|
+
dateStyle: 'medium',
|
|
80
|
+
...(withTime ? { timeStyle: 'short' as const } : {}),
|
|
81
|
+
}).format(d)
|
|
82
|
+
}
|