synstate-react-hooks 6.2.0 → 6.2.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 +2 -2
- package/dist/use-observable-effect.d.mts +6 -6
- package/dist/use-observable-effect.mjs.map +1 -1
- package/dist/use-value-as-observable.d.mts +14 -0
- package/dist/use-value-as-observable.d.mts.map +1 -1
- package/dist/use-value-as-observable.mjs +52 -12
- package/dist/use-value-as-observable.mjs.map +1 -1
- package/package.json +8 -8
- package/src/use-observable-effect.mts +8 -8
- package/src/use-value-as-observable.mts +67 -18
package/README.md
CHANGED
|
@@ -137,8 +137,8 @@ const [useCount, setCount, { state: count$ }] = createState(0);
|
|
|
137
137
|
// Derived InitializedObservable — no fallback needed.
|
|
138
138
|
const doubled$ = count$.pipe(map((n) => n * 2));
|
|
139
139
|
|
|
140
|
-
// Imagine an Observable<string> that has no initial value
|
|
141
|
-
//
|
|
140
|
+
// Imagine an Observable<string> that has no initial value (e.g. one waiting on
|
|
141
|
+
// an async source).
|
|
142
142
|
declare const userName$: Observable<string>;
|
|
143
143
|
|
|
144
144
|
const Profile = (): React.JSX.Element => {
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { Observable } from 'synstate';
|
|
2
2
|
/**
|
|
3
|
-
* Subscribes `subscriptionFn` to `observable$` while the component is
|
|
4
|
-
*
|
|
3
|
+
* Subscribes `subscriptionFn` to `observable$` while the component is mounted,
|
|
4
|
+
* and unsubscribes when it unmounts.
|
|
5
5
|
*
|
|
6
|
-
* Each value is passed to the `subscriptionFn` of the latest render, so it
|
|
7
|
-
*
|
|
6
|
+
* Each value is passed to the `subscriptionFn` of the latest render, so it sees
|
|
7
|
+
* the current props and state; a new function on every render does not
|
|
8
8
|
* re-subscribe. Passing a different Observable moves the subscription to it,
|
|
9
9
|
* so pass one that keeps its identity across renders (made outside the
|
|
10
|
-
* component, or memoized): one built during render is subscribed again, and
|
|
11
|
-
*
|
|
10
|
+
* component, or memoized): one built during render is subscribed again, and an
|
|
11
|
+
* initialized one calls `subscriptionFn` again, on every render.
|
|
12
12
|
*
|
|
13
13
|
* @param observable$ The Observable to subscribe to.
|
|
14
14
|
* @param subscriptionFn Called with each value `observable$` emits.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-observable-effect.mjs","sourceRoot":"","sources":["../src/use-observable-effect.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,WAA0B,EAC1B,cAA8B,EACxB,EAAE;IACR,MAAM,qBAAqB,GAAG,KAAK,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;IAE3D,
|
|
1
|
+
{"version":3,"file":"use-observable-effect.mjs","sourceRoot":"","sources":["../src/use-observable-effect.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,WAA0B,EAC1B,cAA8B,EACxB,EAAE;IACR,MAAM,qBAAqB,GAAG,KAAK,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;IAE3D,8EAA8E;IAC9E,6DAA6D;IAC7D,KAAK,CAAC,kBAAkB,CAAC,GAAG,EAAE;QAC5B,qBAAqB,CAAC,OAAO,GAAG,cAAc,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,MAAM,CAAC,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE;YACpC,qBAAqB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;QAEH,OAAO,GAAG,EAAE;YACV,CAAC,CAAC,WAAW,EAAE,CAAC;QAClB,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC;AACpB,CAAC,CAAC"}
|
|
@@ -1,3 +1,17 @@
|
|
|
1
1
|
import { type Observable } from 'synstate';
|
|
2
|
+
/**
|
|
3
|
+
* Returns an observable that holds `input` and emits each new `input` the
|
|
4
|
+
* component renders with. It completes when the component unmounts, in a timer
|
|
5
|
+
* of 0 ms.
|
|
6
|
+
*
|
|
7
|
+
* React 18 and later run an effect's cleanup without unmounting in two cases.
|
|
8
|
+
* StrictMode runs it once after mount in development and sets the effect up
|
|
9
|
+
* again at once; completing only in a timer after the cleanup lets that setup
|
|
10
|
+
* keep the same observable. `<Activity mode="hidden">` (React 19.2) runs it and
|
|
11
|
+
* keeps the component's state; the observable completes, and is replaced with a
|
|
12
|
+
* new one holding the latest value when the component renders or is shown
|
|
13
|
+
* again. So read the observable from each render rather than keeping the first
|
|
14
|
+
* one.
|
|
15
|
+
*/
|
|
2
16
|
export declare const useValueAsObservable: <A>(input: A) => Observable<A>;
|
|
3
17
|
//# sourceMappingURL=use-value-as-observable.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-value-as-observable.d.mts","sourceRoot":"","sources":["../src/use-value-as-observable.mts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"use-value-as-observable.d.mts","sourceRoot":"","sources":["../src/use-value-as-observable.mts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,UAAU,EAEhB,MAAM,UAAU,CAAC;AAElB;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAU,CAAC,KAAG,UAAU,CAAC,CAAC,CAiD/D,CAAC"}
|
|
@@ -1,18 +1,58 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { source, withInitialValue, } from 'synstate';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
3
17
|
export const useValueAsObservable = (input) => {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
React.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
18
|
+
const [observables, setObservables] = React.useState(() => createObservables(input));
|
|
19
|
+
|
|
20
|
+
const mut_setups = React.useRef(0);
|
|
21
|
+
|
|
22
|
+
if (observables.state.isCompleted) {
|
|
23
|
+
setObservables(createObservables(input));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
13
27
|
React.useEffect(() => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
28
|
+
mut_setups.current += 1;
|
|
29
|
+
if (observables.state.isCompleted) {
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
setObservables(createObservables(input));
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return () => {
|
|
37
|
+
const setups = mut_setups.current;
|
|
38
|
+
setTimeout(() => {
|
|
39
|
+
if (mut_setups.current === setups) {
|
|
40
|
+
observables.state.complete();
|
|
41
|
+
}
|
|
42
|
+
}, 0);
|
|
43
|
+
};
|
|
44
|
+
}, [observables, input]);
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
|
|
47
|
+
if (!observables.state.isCompleted &&
|
|
48
|
+
!Object.is(observables.state.getSnapshot().value, input)) {
|
|
49
|
+
observables.source.next(input);
|
|
50
|
+
}
|
|
51
|
+
}, [input, observables]);
|
|
52
|
+
return observables.state;
|
|
53
|
+
};
|
|
54
|
+
const createObservables = (input) => {
|
|
55
|
+
const source$ = source();
|
|
56
|
+
return { source: source$, state: source$.pipe(withInitialValue(input)) };
|
|
17
57
|
};
|
|
18
58
|
//# sourceMappingURL=use-value-as-observable.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-value-as-observable.mjs","sourceRoot":"","sources":["../src/use-value-as-observable.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EACL,MAAM,EACN,gBAAgB,
|
|
1
|
+
{"version":3,"file":"use-value-as-observable.mjs","sourceRoot":"","sources":["../src/use-value-as-observable.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EACL,MAAM,EACN,gBAAgB,GAIjB,MAAM,UAAU,CAAC;AAElB;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAK,KAAQ,EAAiB,EAAE;IAClE,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,EAAE,CACxD,iBAAiB,CAAC,KAAK,CAAC,CACzB,CAAC;IAEF,8EAA8E;IAC9E,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAEnC,+EAA+E;IAC/E,IAAI,WAAW,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;QAClC,cAAc,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3C,CAAC;IAED,4EAA4E;IAC5E,sDAAsD;IACtD,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;QAExB,IAAI,WAAW,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;YAClC,gEAAgE;YAChE,oEAAoE;YACpE,8CAA8C;YAC9C,cAAc,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC;YAEzC,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,OAAO,GAAG,EAAE;YACV,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC;YAElC,UAAU,CAAC,GAAG,EAAE;gBACd,IAAI,UAAU,CAAC,OAAO,KAAK,MAAM,EAAE,CAAC;oBAClC,WAAW,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;gBAC/B,CAAC;YACH,CAAC,EAAE,CAAC,CAAC,CAAC;QACR,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC;IAEzB,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,8DAA8D;QAC9D,IACE,CAAC,WAAW,CAAC,KAAK,CAAC,WAAW;YAC9B,CAAC,MAAM,CAAC,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,EACxD,CAAC;YACD,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC;IACH,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC;IAEzB,OAAO,WAAW,CAAC,KAAK,CAAC;AAC3B,CAAC,CAAC;AAOF,MAAM,iBAAiB,GAAG,CAAK,KAAQ,EAAkB,EAAE;IACzD,MAAM,OAAO,GAAG,MAAM,EAAK,CAAC;IAE5B,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;AAC3E,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "synstate-react-hooks",
|
|
3
|
-
"version": "6.2.
|
|
3
|
+
"version": "6.2.1",
|
|
4
4
|
"description": "Type-safe State Management Library for TypeScript/JavaScript",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,21 +28,21 @@
|
|
|
28
28
|
"LICENSE"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"synstate": "6.
|
|
32
|
-
"ts-data-forge": "^14.10.
|
|
31
|
+
"synstate": "6.2.1",
|
|
32
|
+
"ts-data-forge": "^14.10.2",
|
|
33
33
|
"ts-type-forge": "^9.2.2"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@sumi-lang/cli": "0.1.
|
|
36
|
+
"@sumi-lang/cli": "0.1.5",
|
|
37
37
|
"@testing-library/dom": "10.4.2",
|
|
38
38
|
"@testing-library/react": "16.3.3",
|
|
39
39
|
"@types/react": "19.3.0",
|
|
40
40
|
"@types/react-dom": "19.3.0",
|
|
41
41
|
"@vitest/browser-playwright": "5.0.5",
|
|
42
|
-
"eslint-config-typed": "7.
|
|
43
|
-
"eslint-plugin-ts-data-forge": "0.10.
|
|
44
|
-
"eslint-plugin-ts-fortress": "1.3.
|
|
45
|
-
"eslint-plugin-ts-type-forge": "0.8.
|
|
42
|
+
"eslint-config-typed": "7.3.1",
|
|
43
|
+
"eslint-plugin-ts-data-forge": "0.10.8",
|
|
44
|
+
"eslint-plugin-ts-fortress": "1.3.12",
|
|
45
|
+
"eslint-plugin-ts-type-forge": "0.8.17",
|
|
46
46
|
"playwright": "1.64.1",
|
|
47
47
|
"prettier": "3.9.10",
|
|
48
48
|
"prettier-plugin-organize-imports": "4.3.0",
|
|
@@ -2,15 +2,15 @@ import * as React from 'react';
|
|
|
2
2
|
import type { Observable } from 'synstate';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Subscribes `subscriptionFn` to `observable$` while the component is
|
|
6
|
-
*
|
|
5
|
+
* Subscribes `subscriptionFn` to `observable$` while the component is mounted,
|
|
6
|
+
* and unsubscribes when it unmounts.
|
|
7
7
|
*
|
|
8
|
-
* Each value is passed to the `subscriptionFn` of the latest render, so it
|
|
9
|
-
*
|
|
8
|
+
* Each value is passed to the `subscriptionFn` of the latest render, so it sees
|
|
9
|
+
* the current props and state; a new function on every render does not
|
|
10
10
|
* re-subscribe. Passing a different Observable moves the subscription to it,
|
|
11
11
|
* so pass one that keeps its identity across renders (made outside the
|
|
12
|
-
* component, or memoized): one built during render is subscribed again, and
|
|
13
|
-
*
|
|
12
|
+
* component, or memoized): one built during render is subscribed again, and an
|
|
13
|
+
* initialized one calls `subscriptionFn` again, on every render.
|
|
14
14
|
*
|
|
15
15
|
* @param observable$ The Observable to subscribe to.
|
|
16
16
|
* @param subscriptionFn Called with each value `observable$` emits.
|
|
@@ -21,8 +21,8 @@ export const useObservableEffect = <A,>(
|
|
|
21
21
|
): void => {
|
|
22
22
|
const mut_subscriptionFnRef = React.useRef(subscriptionFn);
|
|
23
23
|
|
|
24
|
-
// Runs before the layout and passive effects of the commit, so a value one
|
|
25
|
-
//
|
|
24
|
+
// Runs before the layout and passive effects of the commit, so a value one of
|
|
25
|
+
// them emits already reaches this render's `subscriptionFn`.
|
|
26
26
|
React.useInsertionEffect(() => {
|
|
27
27
|
mut_subscriptionFnRef.current = subscriptionFn;
|
|
28
28
|
});
|
|
@@ -2,34 +2,83 @@ import * as React from 'react';
|
|
|
2
2
|
import {
|
|
3
3
|
source,
|
|
4
4
|
withInitialValue,
|
|
5
|
+
type InitializedObservable,
|
|
5
6
|
type Observable,
|
|
6
7
|
type SourceObservable,
|
|
7
8
|
} from 'synstate';
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Returns an observable that holds `input` and emits each new `input` the
|
|
12
|
+
* component renders with. It completes when the component unmounts, in a timer
|
|
13
|
+
* of 0 ms.
|
|
14
|
+
*
|
|
15
|
+
* React 18 and later run an effect's cleanup without unmounting in two cases.
|
|
16
|
+
* StrictMode runs it once after mount in development and sets the effect up
|
|
17
|
+
* again at once; completing only in a timer after the cleanup lets that setup
|
|
18
|
+
* keep the same observable. `<Activity mode="hidden">` (React 19.2) runs it and
|
|
19
|
+
* keeps the component's state; the observable completes, and is replaced with a
|
|
20
|
+
* new one holding the latest value when the component renders or is shown
|
|
21
|
+
* again. So read the observable from each render rather than keeping the first
|
|
22
|
+
* one.
|
|
23
|
+
*/
|
|
9
24
|
export const useValueAsObservable = <A,>(input: A): Observable<A> => {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const state = React.useMemo(
|
|
13
|
-
() => sourceObservable.pipe(withInitialValue(input)),
|
|
14
|
-
[],
|
|
25
|
+
const [observables, setObservables] = React.useState(() =>
|
|
26
|
+
createObservables(input),
|
|
15
27
|
);
|
|
16
28
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
/** Bumped by every setup, so that a completion scheduled before it lapses. */
|
|
30
|
+
const mut_setups = React.useRef(0);
|
|
31
|
+
|
|
32
|
+
// Replaced during render, so that what renders below gets the new one at once.
|
|
33
|
+
if (observables.state.isCompleted) {
|
|
34
|
+
setObservables(createObservables(input));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// A new `input` runs this effect's cleanup and setup again at once, and the
|
|
38
|
+
// setup cancels the completion the cleanup scheduled.
|
|
39
|
+
React.useEffect(() => {
|
|
40
|
+
mut_setups.current += 1;
|
|
41
|
+
|
|
42
|
+
if (observables.state.isCompleted) {
|
|
43
|
+
// Shown again with no render: start from the latest input React
|
|
44
|
+
// committed, which a render while hidden may have changed after the
|
|
45
|
+
// completed observable stopped taking values.
|
|
46
|
+
setObservables(createObservables(input));
|
|
23
47
|
|
|
24
|
-
|
|
25
|
-
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
26
50
|
|
|
27
|
-
return
|
|
28
|
-
|
|
51
|
+
return () => {
|
|
52
|
+
const setups = mut_setups.current;
|
|
53
|
+
|
|
54
|
+
setTimeout(() => {
|
|
55
|
+
if (mut_setups.current === setups) {
|
|
56
|
+
observables.state.complete();
|
|
57
|
+
}
|
|
58
|
+
}, 0);
|
|
59
|
+
};
|
|
60
|
+
}, [observables, input]);
|
|
29
61
|
|
|
30
62
|
React.useEffect(() => {
|
|
31
|
-
|
|
32
|
-
|
|
63
|
+
// The observable already holds the value it was created with.
|
|
64
|
+
if (
|
|
65
|
+
!observables.state.isCompleted &&
|
|
66
|
+
!Object.is(observables.state.getSnapshot().value, input)
|
|
67
|
+
) {
|
|
68
|
+
observables.source.next(input);
|
|
69
|
+
}
|
|
70
|
+
}, [input, observables]);
|
|
71
|
+
|
|
72
|
+
return observables.state;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type Observables<A> = Readonly<{
|
|
76
|
+
source: SourceObservable<A>;
|
|
77
|
+
state: InitializedObservable<A>;
|
|
78
|
+
}>;
|
|
79
|
+
|
|
80
|
+
const createObservables = <A,>(input: A): Observables<A> => {
|
|
81
|
+
const source$ = source<A>();
|
|
33
82
|
|
|
34
|
-
return state;
|
|
83
|
+
return { source: source$, state: source$.pipe(withInitialValue(input)) };
|
|
35
84
|
};
|