@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.
@@ -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":"AAAA,OAAO,EAAE,oBAAoB,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAE3E,OAAO,EAAE,GAAG,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAgB3D,SAAS,MAAM,CAAI,GAAY;IAC7B,OAAO,GAAG,KAAK,IAAI,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,KAAK,IAAI,GAAG,IAAI,OAAQ,GAAsB,CAAC,GAAG,KAAK,UAAU,CAAC;AACtH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,aAAa,CAC3B,WAA0B,EAC1B,WAAoB;IAEpB,MAAM,QAAQ,GAAG,MAAM,CAAI,WAAW,EAAE,CAAC,CAAC;IAE1C,MAAM,SAAS,GAAG,WAAW,CAC3B,CAAC,aAAyB,EAAE,EAAE;QAC5B,MAAM,YAAY,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,EAAE;YACtD,QAAQ,CAAC,OAAO,GAAG,QAAQ,CAAC;YAC5B,aAAa,EAAE,CAAC;QAClB,CAAC,CAAC,CAAC;QAEH,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC;IAC1C,CAAC,EACD,CAAC,WAAW,CAAC,CACd,CAAC;IAEF,MAAM,eAAe,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAEhE,OAAO,oBAAoB,CAAC,SAAS,EAAE,eAAe,EAAE,eAAe,CAAC,CAAC;AAC3E,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,MAAM,UAAU,aAAa,CAC3B,IAAO;IAEP,0DAA0D;IAC1D,MAAM,QAAQ,GAAG,MAAM,CACrB,MAAM,CAAqB,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAE,SAAgC,CAClF,CAAC;IAEF,8CAA8C;IAC9C,MAAM,SAAS,GAAG,WAAW,CAC3B,CAAC,aAAyB,EAAE,EAAE;QAC5B,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,EAAE;YAC/C,QAAQ,CAAC,OAAO,GAAG,QAA8B,CAAC;YAClD,aAAa,EAAE,CAAC;QAClB,CAAC,CAAC,CAAC;QAEH,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC;IAC1C,CAAC,EACD,CAAC,IAAI,CAAC,CACP,CAAC;IAEF,4CAA4C;IAC5C,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAE5D,OAAO,oBAAoB,CAAC,SAAS,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC;AACnE,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,MAAM,UAAU,WAAW,CACzB,IAAO,EACP,QAA0C,EAC1C,aAAsC,MAAM,CAAC,EAAE;IAE/C,8DAA8D;IAC9D,MAAM,QAAQ,GAAG,OAAO,CACtB,GAAG,EAAE,CACH,IAAI,CAAC,IAAI,CACP,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,KAA2B,CAAC,CAAC,EACrD,oBAAoB,CAAC,UAAU,CAAC,CACjC,EACH,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,CAAC,CAC7B,CAAC;IAEF,4BAA4B;IAC5B,MAAM,eAAe,GAAG,GAAM,EAAE;QAC9B,IAAI,MAAM,CAAqB,IAAI,CAAC,EAAE,CAAC;YACrC,OAAO,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAC9B,CAAC;QACD,OAAO,SAAc,CAAC;IACxB,CAAC,CAAC;IAEF,wCAAwC;IACxC,MAAM,QAAQ,GAAG,MAAM,CAAI,eAAe,EAAE,CAAC,CAAC;IAE9C,8CAA8C;IAC9C,MAAM,SAAS,GAAG,WAAW,CAC3B,CAAC,aAAyB,EAAE,EAAE;QAC5B,MAAM,YAAY,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,EAAE;YACnD,QAAQ,CAAC,OAAO,GAAG,QAAQ,CAAC;YAC5B,aAAa,EAAE,CAAC;QAClB,CAAC,CAAC,CAAC;QAEH,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC;IAC1C,CAAC,EACD,CAAC,QAAQ,CAAC,CACX,CAAC;IAEF,4CAA4C;IAC5C,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAE5D,OAAO,oBAAoB,CAAC,SAAS,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC;AACnE,CAAC"}
@@ -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";