@montra-interactive/deepstate-react 0.1.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/dist/hooks.d.ts +135 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +181 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
- package/src/hooks.ts +240 -0
- package/src/index.ts +31 -0
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { Observable } from "rxjs";
|
|
2
|
+
/**
|
|
3
|
+
* Type helper to extract the value type from an Observable.
|
|
4
|
+
* Works with deepstate nodes since they extend Observable.
|
|
5
|
+
*/
|
|
6
|
+
type ObservableValue<T> = T extends Observable<infer V> ? V : never;
|
|
7
|
+
/**
|
|
8
|
+
* Hook to subscribe to any Observable and get its current value.
|
|
9
|
+
* Re-renders the component whenever the observable emits a new value.
|
|
10
|
+
*
|
|
11
|
+
* Works with any RxJS Observable, including deepstate nodes.
|
|
12
|
+
*
|
|
13
|
+
* @param observable$ - Any RxJS Observable
|
|
14
|
+
* @param getSnapshot - Function to get the current value (required for plain observables)
|
|
15
|
+
* @returns The current value of the observable
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* import { useObservable } from 'deepstate-react';
|
|
20
|
+
* import { BehaviorSubject } from 'rxjs';
|
|
21
|
+
*
|
|
22
|
+
* const count$ = new BehaviorSubject(0);
|
|
23
|
+
*
|
|
24
|
+
* function Counter() {
|
|
25
|
+
* const count = useObservable(count$, () => count$.getValue());
|
|
26
|
+
* return <span>{count}</span>;
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function useObservable<T>(observable$: Observable<T>, getSnapshot: () => T): T;
|
|
31
|
+
/**
|
|
32
|
+
* Hook to get the current value of a deepstate node.
|
|
33
|
+
* Re-renders the component whenever the node's value changes.
|
|
34
|
+
*
|
|
35
|
+
* This is the primary hook for using deepstate in React.
|
|
36
|
+
* Works with any deepstate node: RxLeaf, RxObject, RxArray, or RxNullable.
|
|
37
|
+
*
|
|
38
|
+
* Uses React 18's useSyncExternalStore for concurrent-mode safety.
|
|
39
|
+
*
|
|
40
|
+
* @param node - A deepstate node (any reactive property from your state)
|
|
41
|
+
* @returns The current value of the node (deeply readonly)
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* import { state } from 'deepstate';
|
|
46
|
+
* import { useStateValue } from 'deepstate-react';
|
|
47
|
+
*
|
|
48
|
+
* const store = state({
|
|
49
|
+
* user: { name: 'Alice', age: 30 },
|
|
50
|
+
* items: [{ id: 1, name: 'Item 1' }],
|
|
51
|
+
* count: 0
|
|
52
|
+
* });
|
|
53
|
+
*
|
|
54
|
+
* // Subscribe to a primitive
|
|
55
|
+
* function Counter() {
|
|
56
|
+
* const count = useStateValue(store.count);
|
|
57
|
+
* return <span>{count}</span>;
|
|
58
|
+
* }
|
|
59
|
+
*
|
|
60
|
+
* // Subscribe to an object
|
|
61
|
+
* function UserCard() {
|
|
62
|
+
* const user = useStateValue(store.user);
|
|
63
|
+
* return <div>{user.name}, {user.age}</div>;
|
|
64
|
+
* }
|
|
65
|
+
*
|
|
66
|
+
* // Subscribe to a nested property (fine-grained!)
|
|
67
|
+
* function UserName() {
|
|
68
|
+
* const name = useStateValue(store.user.name);
|
|
69
|
+
* return <span>{name}</span>;
|
|
70
|
+
* }
|
|
71
|
+
*
|
|
72
|
+
* // Subscribe to an array
|
|
73
|
+
* function ItemList() {
|
|
74
|
+
* const items = useStateValue(store.items);
|
|
75
|
+
* return <ul>{items.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export declare function useStateValue<T extends Observable<unknown>>(node: T): ObservableValue<T>;
|
|
80
|
+
/**
|
|
81
|
+
* Hook to derive a value from a deepstate node with a selector function.
|
|
82
|
+
* Only re-renders when the derived value changes (using reference equality by default).
|
|
83
|
+
*
|
|
84
|
+
* Use this when you need to compute/transform a value from state.
|
|
85
|
+
* The selector runs on every emission but only triggers re-render if result changes.
|
|
86
|
+
*
|
|
87
|
+
* Uses React 18's useSyncExternalStore for concurrent-mode safety.
|
|
88
|
+
*
|
|
89
|
+
* @param node - A deepstate node to select from
|
|
90
|
+
* @param selector - Function to derive a value from the node's value
|
|
91
|
+
* @param equalityFn - Optional custom equality function (default: Object.is)
|
|
92
|
+
* @returns The derived value
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```tsx
|
|
96
|
+
* import { state } from 'deepstate';
|
|
97
|
+
* import { useSelector } from 'deepstate-react';
|
|
98
|
+
*
|
|
99
|
+
* const store = state({
|
|
100
|
+
* user: { firstName: 'Alice', lastName: 'Smith', age: 30 },
|
|
101
|
+
* items: [{ id: 1, price: 10 }, { id: 2, price: 20 }]
|
|
102
|
+
* });
|
|
103
|
+
*
|
|
104
|
+
* // Derive a computed value
|
|
105
|
+
* function FullName() {
|
|
106
|
+
* const fullName = useSelector(
|
|
107
|
+
* store.user,
|
|
108
|
+
* user => `${user.firstName} ${user.lastName}`
|
|
109
|
+
* );
|
|
110
|
+
* return <span>{fullName}</span>;
|
|
111
|
+
* }
|
|
112
|
+
*
|
|
113
|
+
* // Derive from an array
|
|
114
|
+
* function TotalPrice() {
|
|
115
|
+
* const total = useSelector(
|
|
116
|
+
* store.items,
|
|
117
|
+
* items => items.reduce((sum, item) => sum + item.price, 0)
|
|
118
|
+
* );
|
|
119
|
+
* return <span>Total: ${total}</span>;
|
|
120
|
+
* }
|
|
121
|
+
*
|
|
122
|
+
* // With custom equality (e.g., for arrays)
|
|
123
|
+
* function ItemIds() {
|
|
124
|
+
* const ids = useSelector(
|
|
125
|
+
* store.items,
|
|
126
|
+
* items => items.map(i => i.id),
|
|
127
|
+
* (a, b) => a.length === b.length && a.every((v, i) => v === b[i])
|
|
128
|
+
* );
|
|
129
|
+
* return <span>{ids.join(', ')}</span>;
|
|
130
|
+
* }
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export declare function useSelector<T extends Observable<unknown>, R>(node: T, selector: (value: ObservableValue<T>) => R, equalityFn?: (a: R, b: R) => boolean): R;
|
|
134
|
+
export {};
|
|
135
|
+
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAGvC;;;GAGG;AACH,KAAK,eAAe,CAAC,CAAC,IAAI,CAAC,SAAS,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAcpE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAC7B,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC,EAC1B,WAAW,EAAE,MAAM,CAAC,GACnB,CAAC,CAkBH;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,UAAU,CAAC,OAAO,CAAC,EACzD,IAAI,EAAE,CAAC,GACN,eAAe,CAAC,CAAC,CAAC,CAuBpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC,EAC1D,IAAI,EAAE,CAAC,EACP,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,EAC1C,UAAU,GAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAmB,GAC9C,CAAC,CAuCH"}
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { useSyncExternalStore, useMemo, useCallback, useRef } from "react";
|
|
2
|
+
import { map, distinctUntilChanged } from "rxjs/operators";
|
|
3
|
+
function hasGet(obj) {
|
|
4
|
+
return obj !== null && typeof obj === "object" && "get" in obj && typeof obj.get === "function";
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Hook to subscribe to any Observable and get its current value.
|
|
8
|
+
* Re-renders the component whenever the observable emits a new value.
|
|
9
|
+
*
|
|
10
|
+
* Works with any RxJS Observable, including deepstate nodes.
|
|
11
|
+
*
|
|
12
|
+
* @param observable$ - Any RxJS Observable
|
|
13
|
+
* @param getSnapshot - Function to get the current value (required for plain observables)
|
|
14
|
+
* @returns The current value of the observable
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* import { useObservable } from 'deepstate-react';
|
|
19
|
+
* import { BehaviorSubject } from 'rxjs';
|
|
20
|
+
*
|
|
21
|
+
* const count$ = new BehaviorSubject(0);
|
|
22
|
+
*
|
|
23
|
+
* function Counter() {
|
|
24
|
+
* const count = useObservable(count$, () => count$.getValue());
|
|
25
|
+
* return <span>{count}</span>;
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function useObservable(observable$, getSnapshot) {
|
|
30
|
+
const valueRef = useRef(getSnapshot());
|
|
31
|
+
const subscribe = useCallback((onStoreChange) => {
|
|
32
|
+
const subscription = observable$.subscribe((newValue) => {
|
|
33
|
+
valueRef.current = newValue;
|
|
34
|
+
onStoreChange();
|
|
35
|
+
});
|
|
36
|
+
return () => subscription.unsubscribe();
|
|
37
|
+
}, [observable$]);
|
|
38
|
+
const getSnapshotMemo = useCallback(() => valueRef.current, []);
|
|
39
|
+
return useSyncExternalStore(subscribe, getSnapshotMemo, getSnapshotMemo);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Hook to get the current value of a deepstate node.
|
|
43
|
+
* Re-renders the component whenever the node's value changes.
|
|
44
|
+
*
|
|
45
|
+
* This is the primary hook for using deepstate in React.
|
|
46
|
+
* Works with any deepstate node: RxLeaf, RxObject, RxArray, or RxNullable.
|
|
47
|
+
*
|
|
48
|
+
* Uses React 18's useSyncExternalStore for concurrent-mode safety.
|
|
49
|
+
*
|
|
50
|
+
* @param node - A deepstate node (any reactive property from your state)
|
|
51
|
+
* @returns The current value of the node (deeply readonly)
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```tsx
|
|
55
|
+
* import { state } from 'deepstate';
|
|
56
|
+
* import { useStateValue } from 'deepstate-react';
|
|
57
|
+
*
|
|
58
|
+
* const store = state({
|
|
59
|
+
* user: { name: 'Alice', age: 30 },
|
|
60
|
+
* items: [{ id: 1, name: 'Item 1' }],
|
|
61
|
+
* count: 0
|
|
62
|
+
* });
|
|
63
|
+
*
|
|
64
|
+
* // Subscribe to a primitive
|
|
65
|
+
* function Counter() {
|
|
66
|
+
* const count = useStateValue(store.count);
|
|
67
|
+
* return <span>{count}</span>;
|
|
68
|
+
* }
|
|
69
|
+
*
|
|
70
|
+
* // Subscribe to an object
|
|
71
|
+
* function UserCard() {
|
|
72
|
+
* const user = useStateValue(store.user);
|
|
73
|
+
* return <div>{user.name}, {user.age}</div>;
|
|
74
|
+
* }
|
|
75
|
+
*
|
|
76
|
+
* // Subscribe to a nested property (fine-grained!)
|
|
77
|
+
* function UserName() {
|
|
78
|
+
* const name = useStateValue(store.user.name);
|
|
79
|
+
* return <span>{name}</span>;
|
|
80
|
+
* }
|
|
81
|
+
*
|
|
82
|
+
* // Subscribe to an array
|
|
83
|
+
* function ItemList() {
|
|
84
|
+
* const items = useStateValue(store.items);
|
|
85
|
+
* return <ul>{items.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function useStateValue(node) {
|
|
90
|
+
// Ref to hold the current value - updated by subscription
|
|
91
|
+
const valueRef = useRef(hasGet(node) ? node.get() : undefined);
|
|
92
|
+
// Subscribe callback for useSyncExternalStore
|
|
93
|
+
const subscribe = useCallback((onStoreChange) => {
|
|
94
|
+
const subscription = node.subscribe((newValue) => {
|
|
95
|
+
valueRef.current = newValue;
|
|
96
|
+
onStoreChange();
|
|
97
|
+
});
|
|
98
|
+
return () => subscription.unsubscribe();
|
|
99
|
+
}, [node]);
|
|
100
|
+
// Get snapshot - just returns the ref value
|
|
101
|
+
const getSnapshot = useCallback(() => valueRef.current, []);
|
|
102
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Hook to derive a value from a deepstate node with a selector function.
|
|
106
|
+
* Only re-renders when the derived value changes (using reference equality by default).
|
|
107
|
+
*
|
|
108
|
+
* Use this when you need to compute/transform a value from state.
|
|
109
|
+
* The selector runs on every emission but only triggers re-render if result changes.
|
|
110
|
+
*
|
|
111
|
+
* Uses React 18's useSyncExternalStore for concurrent-mode safety.
|
|
112
|
+
*
|
|
113
|
+
* @param node - A deepstate node to select from
|
|
114
|
+
* @param selector - Function to derive a value from the node's value
|
|
115
|
+
* @param equalityFn - Optional custom equality function (default: Object.is)
|
|
116
|
+
* @returns The derived value
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```tsx
|
|
120
|
+
* import { state } from 'deepstate';
|
|
121
|
+
* import { useSelector } from 'deepstate-react';
|
|
122
|
+
*
|
|
123
|
+
* const store = state({
|
|
124
|
+
* user: { firstName: 'Alice', lastName: 'Smith', age: 30 },
|
|
125
|
+
* items: [{ id: 1, price: 10 }, { id: 2, price: 20 }]
|
|
126
|
+
* });
|
|
127
|
+
*
|
|
128
|
+
* // Derive a computed value
|
|
129
|
+
* function FullName() {
|
|
130
|
+
* const fullName = useSelector(
|
|
131
|
+
* store.user,
|
|
132
|
+
* user => `${user.firstName} ${user.lastName}`
|
|
133
|
+
* );
|
|
134
|
+
* return <span>{fullName}</span>;
|
|
135
|
+
* }
|
|
136
|
+
*
|
|
137
|
+
* // Derive from an array
|
|
138
|
+
* function TotalPrice() {
|
|
139
|
+
* const total = useSelector(
|
|
140
|
+
* store.items,
|
|
141
|
+
* items => items.reduce((sum, item) => sum + item.price, 0)
|
|
142
|
+
* );
|
|
143
|
+
* return <span>Total: ${total}</span>;
|
|
144
|
+
* }
|
|
145
|
+
*
|
|
146
|
+
* // With custom equality (e.g., for arrays)
|
|
147
|
+
* function ItemIds() {
|
|
148
|
+
* const ids = useSelector(
|
|
149
|
+
* store.items,
|
|
150
|
+
* items => items.map(i => i.id),
|
|
151
|
+
* (a, b) => a.length === b.length && a.every((v, i) => v === b[i])
|
|
152
|
+
* );
|
|
153
|
+
* return <span>{ids.join(', ')}</span>;
|
|
154
|
+
* }
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export function useSelector(node, selector, equalityFn = Object.is) {
|
|
158
|
+
// Create derived observable that applies selector and dedupes
|
|
159
|
+
const derived$ = useMemo(() => node.pipe(map((value) => selector(value)), distinctUntilChanged(equalityFn)), [node, selector, equalityFn]);
|
|
160
|
+
// Get initial derived value
|
|
161
|
+
const getInitialValue = () => {
|
|
162
|
+
if (hasGet(node)) {
|
|
163
|
+
return selector(node.get());
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
};
|
|
167
|
+
// Ref to hold the current derived value
|
|
168
|
+
const valueRef = useRef(getInitialValue());
|
|
169
|
+
// Subscribe callback for useSyncExternalStore
|
|
170
|
+
const subscribe = useCallback((onStoreChange) => {
|
|
171
|
+
const subscription = derived$.subscribe((newValue) => {
|
|
172
|
+
valueRef.current = newValue;
|
|
173
|
+
onStoreChange();
|
|
174
|
+
});
|
|
175
|
+
return () => subscription.unsubscribe();
|
|
176
|
+
}, [derived$]);
|
|
177
|
+
// Get snapshot - just returns the ref value
|
|
178
|
+
const getSnapshot = useCallback(() => valueRef.current, []);
|
|
179
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=hooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.js","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* deepstate-react - React bindings for deepstate
|
|
3
|
+
*
|
|
4
|
+
* Provides hooks for using deepstate reactive state in React components.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* import { state } from 'deepstate';
|
|
9
|
+
* import { useStateValue, useSelector } from 'deepstate-react';
|
|
10
|
+
*
|
|
11
|
+
* const store = state({ user: { name: 'Alice', age: 30 }, count: 0 });
|
|
12
|
+
*
|
|
13
|
+
* function UserName() {
|
|
14
|
+
* const name = useStateValue(store.user.name);
|
|
15
|
+
* return <span>{name}</span>;
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* function UserSummary() {
|
|
19
|
+
* const summary = useSelector(store.user, user => `${user.name} (${user.age})`);
|
|
20
|
+
* return <span>{summary}</span>;
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export { useStateValue, useSelector, useObservable, } from "./hooks";
|
|
25
|
+
export type { Observable } from "rxjs";
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EACL,aAAa,EACb,WAAW,EACX,aAAa,GACd,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// src/hooks.ts
|
|
2
|
+
import { useSyncExternalStore, useMemo, useCallback, useRef } from "react";
|
|
3
|
+
import { map, distinctUntilChanged } from "rxjs/operators";
|
|
4
|
+
function hasGet(obj) {
|
|
5
|
+
return obj !== null && typeof obj === "object" && "get" in obj && typeof obj.get === "function";
|
|
6
|
+
}
|
|
7
|
+
function useObservable(observable$, getSnapshot) {
|
|
8
|
+
const valueRef = useRef(getSnapshot());
|
|
9
|
+
const subscribe = useCallback((onStoreChange) => {
|
|
10
|
+
const subscription = observable$.subscribe((newValue) => {
|
|
11
|
+
valueRef.current = newValue;
|
|
12
|
+
onStoreChange();
|
|
13
|
+
});
|
|
14
|
+
return () => subscription.unsubscribe();
|
|
15
|
+
}, [observable$]);
|
|
16
|
+
const getSnapshotMemo = useCallback(() => valueRef.current, []);
|
|
17
|
+
return useSyncExternalStore(subscribe, getSnapshotMemo, getSnapshotMemo);
|
|
18
|
+
}
|
|
19
|
+
function useStateValue(node) {
|
|
20
|
+
const valueRef = useRef(hasGet(node) ? node.get() : undefined);
|
|
21
|
+
const subscribe = useCallback((onStoreChange) => {
|
|
22
|
+
const subscription = node.subscribe((newValue) => {
|
|
23
|
+
valueRef.current = newValue;
|
|
24
|
+
onStoreChange();
|
|
25
|
+
});
|
|
26
|
+
return () => subscription.unsubscribe();
|
|
27
|
+
}, [node]);
|
|
28
|
+
const getSnapshot = useCallback(() => valueRef.current, []);
|
|
29
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
30
|
+
}
|
|
31
|
+
function useSelector(node, selector, equalityFn = Object.is) {
|
|
32
|
+
const derived$ = useMemo(() => node.pipe(map((value) => selector(value)), distinctUntilChanged(equalityFn)), [node, selector, equalityFn]);
|
|
33
|
+
const getInitialValue = () => {
|
|
34
|
+
if (hasGet(node)) {
|
|
35
|
+
return selector(node.get());
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
};
|
|
39
|
+
const valueRef = useRef(getInitialValue());
|
|
40
|
+
const subscribe = useCallback((onStoreChange) => {
|
|
41
|
+
const subscription = derived$.subscribe((newValue) => {
|
|
42
|
+
valueRef.current = newValue;
|
|
43
|
+
onStoreChange();
|
|
44
|
+
});
|
|
45
|
+
return () => subscription.unsubscribe();
|
|
46
|
+
}, [derived$]);
|
|
47
|
+
const getSnapshot = useCallback(() => valueRef.current, []);
|
|
48
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
useStateValue,
|
|
52
|
+
useSelector,
|
|
53
|
+
useObservable
|
|
54
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EACL,aAAa,EACb,WAAW,EACX,aAAa,GACd,MAAM,YAAY,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@montra-interactive/deepstate-react",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "React bindings for deepstate - Proxy-based reactive state management with RxJS.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"hooks",
|
|
8
|
+
"state",
|
|
9
|
+
"state-management",
|
|
10
|
+
"rxjs",
|
|
11
|
+
"reactive",
|
|
12
|
+
"observable",
|
|
13
|
+
"deepstate"
|
|
14
|
+
],
|
|
15
|
+
"author": "Ronnie Magatti",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/Montra-Interactive/deepstate",
|
|
20
|
+
"directory": "packages/react"
|
|
21
|
+
},
|
|
22
|
+
"module": "dist/index.js",
|
|
23
|
+
"main": "dist/index.js",
|
|
24
|
+
"types": "dist/index.d.ts",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target node --external react --external rxjs --external deepstate && tsc -p tsconfig.build.json --emitDeclarationOnly"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"import": "./dist/index.js",
|
|
32
|
+
"require": "./dist/index.js",
|
|
33
|
+
"types": "./dist/index.d.ts"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"src"
|
|
39
|
+
],
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"react": "^18 || ^19",
|
|
42
|
+
"rxjs": "^7",
|
|
43
|
+
"deepstate": "^0.1.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@testing-library/react": "^16.3.1",
|
|
47
|
+
"@types/jsdom": "^27.0.0",
|
|
48
|
+
"@types/react": "^19.2.8",
|
|
49
|
+
"deepstate": "workspace:*",
|
|
50
|
+
"jsdom": "^27.4.0",
|
|
51
|
+
"react": "^19.2.3"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { useSyncExternalStore, useMemo, useCallback, useRef } from "react";
|
|
2
|
+
import type { Observable } from "rxjs";
|
|
3
|
+
import { map, distinctUntilChanged } from "rxjs/operators";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Type helper to extract the value type from an Observable.
|
|
7
|
+
* Works with deepstate nodes since they extend Observable.
|
|
8
|
+
*/
|
|
9
|
+
type ObservableValue<T> = T extends Observable<infer V> ? V : never;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Interface for deepstate nodes that have a synchronous get() method.
|
|
13
|
+
* This is used internally to detect deepstate nodes vs plain observables.
|
|
14
|
+
*/
|
|
15
|
+
interface NodeWithGet<T> {
|
|
16
|
+
get(): T;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function hasGet<T>(obj: unknown): obj is NodeWithGet<T> {
|
|
20
|
+
return obj !== null && typeof obj === "object" && "get" in obj && typeof (obj as NodeWithGet<T>).get === "function";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Hook to subscribe to any Observable and get its current value.
|
|
25
|
+
* Re-renders the component whenever the observable emits a new value.
|
|
26
|
+
*
|
|
27
|
+
* Works with any RxJS Observable, including deepstate nodes.
|
|
28
|
+
*
|
|
29
|
+
* @param observable$ - Any RxJS Observable
|
|
30
|
+
* @param getSnapshot - Function to get the current value (required for plain observables)
|
|
31
|
+
* @returns The current value of the observable
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* import { useObservable } from 'deepstate-react';
|
|
36
|
+
* import { BehaviorSubject } from 'rxjs';
|
|
37
|
+
*
|
|
38
|
+
* const count$ = new BehaviorSubject(0);
|
|
39
|
+
*
|
|
40
|
+
* function Counter() {
|
|
41
|
+
* const count = useObservable(count$, () => count$.getValue());
|
|
42
|
+
* return <span>{count}</span>;
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function useObservable<T>(
|
|
47
|
+
observable$: Observable<T>,
|
|
48
|
+
getSnapshot: () => T
|
|
49
|
+
): T {
|
|
50
|
+
const valueRef = useRef<T>(getSnapshot());
|
|
51
|
+
|
|
52
|
+
const subscribe = useCallback(
|
|
53
|
+
(onStoreChange: () => void) => {
|
|
54
|
+
const subscription = observable$.subscribe((newValue) => {
|
|
55
|
+
valueRef.current = newValue;
|
|
56
|
+
onStoreChange();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return () => subscription.unsubscribe();
|
|
60
|
+
},
|
|
61
|
+
[observable$]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const getSnapshotMemo = useCallback(() => valueRef.current, []);
|
|
65
|
+
|
|
66
|
+
return useSyncExternalStore(subscribe, getSnapshotMemo, getSnapshotMemo);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Hook to get the current value of a deepstate node.
|
|
71
|
+
* Re-renders the component whenever the node's value changes.
|
|
72
|
+
*
|
|
73
|
+
* This is the primary hook for using deepstate in React.
|
|
74
|
+
* Works with any deepstate node: RxLeaf, RxObject, RxArray, or RxNullable.
|
|
75
|
+
*
|
|
76
|
+
* Uses React 18's useSyncExternalStore for concurrent-mode safety.
|
|
77
|
+
*
|
|
78
|
+
* @param node - A deepstate node (any reactive property from your state)
|
|
79
|
+
* @returns The current value of the node (deeply readonly)
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```tsx
|
|
83
|
+
* import { state } from 'deepstate';
|
|
84
|
+
* import { useStateValue } from 'deepstate-react';
|
|
85
|
+
*
|
|
86
|
+
* const store = state({
|
|
87
|
+
* user: { name: 'Alice', age: 30 },
|
|
88
|
+
* items: [{ id: 1, name: 'Item 1' }],
|
|
89
|
+
* count: 0
|
|
90
|
+
* });
|
|
91
|
+
*
|
|
92
|
+
* // Subscribe to a primitive
|
|
93
|
+
* function Counter() {
|
|
94
|
+
* const count = useStateValue(store.count);
|
|
95
|
+
* return <span>{count}</span>;
|
|
96
|
+
* }
|
|
97
|
+
*
|
|
98
|
+
* // Subscribe to an object
|
|
99
|
+
* function UserCard() {
|
|
100
|
+
* const user = useStateValue(store.user);
|
|
101
|
+
* return <div>{user.name}, {user.age}</div>;
|
|
102
|
+
* }
|
|
103
|
+
*
|
|
104
|
+
* // Subscribe to a nested property (fine-grained!)
|
|
105
|
+
* function UserName() {
|
|
106
|
+
* const name = useStateValue(store.user.name);
|
|
107
|
+
* return <span>{name}</span>;
|
|
108
|
+
* }
|
|
109
|
+
*
|
|
110
|
+
* // Subscribe to an array
|
|
111
|
+
* function ItemList() {
|
|
112
|
+
* const items = useStateValue(store.items);
|
|
113
|
+
* return <ul>{items.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
|
|
114
|
+
* }
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
export function useStateValue<T extends Observable<unknown>>(
|
|
118
|
+
node: T
|
|
119
|
+
): ObservableValue<T> {
|
|
120
|
+
// Ref to hold the current value - updated by subscription
|
|
121
|
+
const valueRef = useRef<ObservableValue<T>>(
|
|
122
|
+
hasGet<ObservableValue<T>>(node) ? node.get() : (undefined as ObservableValue<T>)
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Subscribe callback for useSyncExternalStore
|
|
126
|
+
const subscribe = useCallback(
|
|
127
|
+
(onStoreChange: () => void) => {
|
|
128
|
+
const subscription = node.subscribe((newValue) => {
|
|
129
|
+
valueRef.current = newValue as ObservableValue<T>;
|
|
130
|
+
onStoreChange();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return () => subscription.unsubscribe();
|
|
134
|
+
},
|
|
135
|
+
[node]
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// Get snapshot - just returns the ref value
|
|
139
|
+
const getSnapshot = useCallback(() => valueRef.current, []);
|
|
140
|
+
|
|
141
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Hook to derive a value from a deepstate node with a selector function.
|
|
146
|
+
* Only re-renders when the derived value changes (using reference equality by default).
|
|
147
|
+
*
|
|
148
|
+
* Use this when you need to compute/transform a value from state.
|
|
149
|
+
* The selector runs on every emission but only triggers re-render if result changes.
|
|
150
|
+
*
|
|
151
|
+
* Uses React 18's useSyncExternalStore for concurrent-mode safety.
|
|
152
|
+
*
|
|
153
|
+
* @param node - A deepstate node to select from
|
|
154
|
+
* @param selector - Function to derive a value from the node's value
|
|
155
|
+
* @param equalityFn - Optional custom equality function (default: Object.is)
|
|
156
|
+
* @returns The derived value
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```tsx
|
|
160
|
+
* import { state } from 'deepstate';
|
|
161
|
+
* import { useSelector } from 'deepstate-react';
|
|
162
|
+
*
|
|
163
|
+
* const store = state({
|
|
164
|
+
* user: { firstName: 'Alice', lastName: 'Smith', age: 30 },
|
|
165
|
+
* items: [{ id: 1, price: 10 }, { id: 2, price: 20 }]
|
|
166
|
+
* });
|
|
167
|
+
*
|
|
168
|
+
* // Derive a computed value
|
|
169
|
+
* function FullName() {
|
|
170
|
+
* const fullName = useSelector(
|
|
171
|
+
* store.user,
|
|
172
|
+
* user => `${user.firstName} ${user.lastName}`
|
|
173
|
+
* );
|
|
174
|
+
* return <span>{fullName}</span>;
|
|
175
|
+
* }
|
|
176
|
+
*
|
|
177
|
+
* // Derive from an array
|
|
178
|
+
* function TotalPrice() {
|
|
179
|
+
* const total = useSelector(
|
|
180
|
+
* store.items,
|
|
181
|
+
* items => items.reduce((sum, item) => sum + item.price, 0)
|
|
182
|
+
* );
|
|
183
|
+
* return <span>Total: ${total}</span>;
|
|
184
|
+
* }
|
|
185
|
+
*
|
|
186
|
+
* // With custom equality (e.g., for arrays)
|
|
187
|
+
* function ItemIds() {
|
|
188
|
+
* const ids = useSelector(
|
|
189
|
+
* store.items,
|
|
190
|
+
* items => items.map(i => i.id),
|
|
191
|
+
* (a, b) => a.length === b.length && a.every((v, i) => v === b[i])
|
|
192
|
+
* );
|
|
193
|
+
* return <span>{ids.join(', ')}</span>;
|
|
194
|
+
* }
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
export function useSelector<T extends Observable<unknown>, R>(
|
|
198
|
+
node: T,
|
|
199
|
+
selector: (value: ObservableValue<T>) => R,
|
|
200
|
+
equalityFn: (a: R, b: R) => boolean = Object.is
|
|
201
|
+
): R {
|
|
202
|
+
// Create derived observable that applies selector and dedupes
|
|
203
|
+
const derived$ = useMemo(
|
|
204
|
+
() =>
|
|
205
|
+
node.pipe(
|
|
206
|
+
map((value) => selector(value as ObservableValue<T>)),
|
|
207
|
+
distinctUntilChanged(equalityFn)
|
|
208
|
+
),
|
|
209
|
+
[node, selector, equalityFn]
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// Get initial derived value
|
|
213
|
+
const getInitialValue = (): R => {
|
|
214
|
+
if (hasGet<ObservableValue<T>>(node)) {
|
|
215
|
+
return selector(node.get());
|
|
216
|
+
}
|
|
217
|
+
return undefined as R;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Ref to hold the current derived value
|
|
221
|
+
const valueRef = useRef<R>(getInitialValue());
|
|
222
|
+
|
|
223
|
+
// Subscribe callback for useSyncExternalStore
|
|
224
|
+
const subscribe = useCallback(
|
|
225
|
+
(onStoreChange: () => void) => {
|
|
226
|
+
const subscription = derived$.subscribe((newValue) => {
|
|
227
|
+
valueRef.current = newValue;
|
|
228
|
+
onStoreChange();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return () => subscription.unsubscribe();
|
|
232
|
+
},
|
|
233
|
+
[derived$]
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// Get snapshot - just returns the ref value
|
|
237
|
+
const getSnapshot = useCallback(() => valueRef.current, []);
|
|
238
|
+
|
|
239
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
240
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* deepstate-react - React bindings for deepstate
|
|
3
|
+
*
|
|
4
|
+
* Provides hooks for using deepstate reactive state in React components.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* import { state } from 'deepstate';
|
|
9
|
+
* import { useStateValue, useSelector } from 'deepstate-react';
|
|
10
|
+
*
|
|
11
|
+
* const store = state({ user: { name: 'Alice', age: 30 }, count: 0 });
|
|
12
|
+
*
|
|
13
|
+
* function UserName() {
|
|
14
|
+
* const name = useStateValue(store.user.name);
|
|
15
|
+
* return <span>{name}</span>;
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* function UserSummary() {
|
|
19
|
+
* const summary = useSelector(store.user, user => `${user.name} (${user.age})`);
|
|
20
|
+
* return <span>{summary}</span>;
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
useStateValue,
|
|
27
|
+
useSelector,
|
|
28
|
+
useObservable,
|
|
29
|
+
} from "./hooks";
|
|
30
|
+
|
|
31
|
+
export type { Observable } from "rxjs";
|