jotai-state-tree 0.1.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/dist/react.mjs ADDED
@@ -0,0 +1,372 @@
1
+ import {
2
+ __toCommonJS,
3
+ getSnapshot,
4
+ getStateTreeNode,
5
+ hasStateTreeNode,
6
+ init_tree,
7
+ onLifecycleChange,
8
+ onSnapshot,
9
+ tree_exports
10
+ } from "./chunk-XXZK62DD.mjs";
11
+
12
+ // src/react.ts
13
+ init_tree();
14
+ import React, {
15
+ useState,
16
+ useEffect,
17
+ useMemo,
18
+ useRef,
19
+ forwardRef,
20
+ memo,
21
+ useCallback,
22
+ useSyncExternalStore
23
+ } from "react";
24
+ function observer(Component, options) {
25
+ const displayName = Component.displayName || Component.name || "Component";
26
+ const ObserverComponent = memo((props) => {
27
+ const [, forceUpdate] = useState({});
28
+ const disposersRef = useRef(/* @__PURE__ */ new Set());
29
+ const trackedNodesRef = useRef(/* @__PURE__ */ new Set());
30
+ const trackNode = (node) => {
31
+ if (hasStateTreeNode(node) && !trackedNodesRef.current.has(node)) {
32
+ trackedNodesRef.current.add(node);
33
+ const disposer = onSnapshot(node, () => {
34
+ forceUpdate({});
35
+ });
36
+ disposersRef.current.add(disposer);
37
+ }
38
+ };
39
+ const createTrackingProxy = (target) => {
40
+ if (!target || typeof target !== "object") return target;
41
+ if (hasStateTreeNode(target)) {
42
+ trackNode(target);
43
+ }
44
+ return new Proxy(target, {
45
+ get(obj, prop) {
46
+ const value = obj[prop];
47
+ if (value && typeof value === "object" && hasStateTreeNode(value)) {
48
+ trackNode(value);
49
+ return createTrackingProxy(value);
50
+ }
51
+ return value;
52
+ }
53
+ });
54
+ };
55
+ useEffect(() => {
56
+ return () => {
57
+ disposersRef.current.forEach((d) => d());
58
+ disposersRef.current.clear();
59
+ trackedNodesRef.current.clear();
60
+ };
61
+ }, []);
62
+ const trackedProps = useMemo(() => {
63
+ const tracked = {};
64
+ for (const [key, value] of Object.entries(props)) {
65
+ if (value && typeof value === "object") {
66
+ tracked[key] = createTrackingProxy(value);
67
+ } else {
68
+ tracked[key] = value;
69
+ }
70
+ }
71
+ return tracked;
72
+ }, [props]);
73
+ return React.createElement(Component, trackedProps);
74
+ });
75
+ ObserverComponent.displayName = `Observer(${displayName})`;
76
+ if (options?.forwardRef) {
77
+ const ForwardedComponent = forwardRef((props, ref) => {
78
+ const propsWithRef = Object.assign({}, props, { ref });
79
+ return React.createElement(
80
+ ObserverComponent,
81
+ propsWithRef
82
+ );
83
+ });
84
+ ForwardedComponent.displayName = `ForwardRef(${displayName})`;
85
+ return ForwardedComponent;
86
+ }
87
+ return ObserverComponent;
88
+ }
89
+ var Observer = observer(({ children }) => {
90
+ return React.createElement(React.Fragment, null, children());
91
+ });
92
+ function useObserver(fn) {
93
+ const [, forceUpdate] = useState({});
94
+ const disposersRef = useRef([]);
95
+ const trackedNodes = useRef(/* @__PURE__ */ new Set());
96
+ useEffect(() => {
97
+ return () => {
98
+ disposersRef.current.forEach((d) => d());
99
+ disposersRef.current = [];
100
+ };
101
+ }, []);
102
+ const result = useMemo(() => {
103
+ trackedNodes.current.clear();
104
+ disposersRef.current.forEach((d) => d());
105
+ disposersRef.current = [];
106
+ const value = fn();
107
+ return value;
108
+ }, [fn]);
109
+ return result;
110
+ }
111
+ function useLocalObservable(initializer, dependencies = []) {
112
+ const [, forceUpdate] = useState({});
113
+ const storeRef = useRef(null);
114
+ const disposerRef = useRef(null);
115
+ if (storeRef.current === null) {
116
+ storeRef.current = initializer();
117
+ if (hasStateTreeNode(storeRef.current)) {
118
+ disposerRef.current = onSnapshot(storeRef.current, () => {
119
+ forceUpdate({});
120
+ });
121
+ }
122
+ }
123
+ useEffect(() => {
124
+ return () => {
125
+ disposerRef.current?.();
126
+ };
127
+ }, []);
128
+ useEffect(() => {
129
+ if (dependencies.length > 0) {
130
+ disposerRef.current?.();
131
+ storeRef.current = initializer();
132
+ if (hasStateTreeNode(storeRef.current)) {
133
+ disposerRef.current = onSnapshot(storeRef.current, () => {
134
+ forceUpdate({});
135
+ });
136
+ }
137
+ }
138
+ }, dependencies);
139
+ return storeRef.current;
140
+ }
141
+ function useSyncedStore(store) {
142
+ const snapshotRef = useRef(null);
143
+ const subscribe = useCallback(
144
+ (callback) => {
145
+ if (!hasStateTreeNode(store)) {
146
+ return () => {
147
+ };
148
+ }
149
+ return onSnapshot(store, () => {
150
+ snapshotRef.current = getSnapshot(store);
151
+ callback();
152
+ });
153
+ },
154
+ [store]
155
+ );
156
+ const getSnapshotValue = useCallback(() => {
157
+ if (!hasStateTreeNode(store)) {
158
+ return null;
159
+ }
160
+ if (snapshotRef.current === null) {
161
+ snapshotRef.current = getSnapshot(store);
162
+ }
163
+ return snapshotRef.current;
164
+ }, [store]);
165
+ useSyncExternalStore(subscribe, getSnapshotValue, getSnapshotValue);
166
+ return store;
167
+ }
168
+ var StoreContext = React.createContext(
169
+ null
170
+ );
171
+ function Provider({
172
+ store,
173
+ children
174
+ }) {
175
+ const value = useMemo(() => ({ store }), [store]);
176
+ return React.createElement(StoreContext.Provider, { value }, children);
177
+ }
178
+ function useStore() {
179
+ const context = React.useContext(StoreContext);
180
+ if (!context) {
181
+ throw new Error(
182
+ "[jotai-state-tree] useStore must be used within a Provider"
183
+ );
184
+ }
185
+ return context.store;
186
+ }
187
+ function useStoreSnapshot(selector) {
188
+ const store = useStore();
189
+ const [, forceUpdate] = useState({});
190
+ useEffect(() => {
191
+ if (hasStateTreeNode(store)) {
192
+ return onSnapshot(store, () => {
193
+ forceUpdate({});
194
+ });
195
+ }
196
+ return () => {
197
+ };
198
+ }, [store]);
199
+ if (selector) {
200
+ return selector(store);
201
+ }
202
+ return store;
203
+ }
204
+ function createStoreContext() {
205
+ const Context = React.createContext(null);
206
+ function StoreProvider({
207
+ store,
208
+ children
209
+ }) {
210
+ return React.createElement(Context.Provider, { value: store }, children);
211
+ }
212
+ function useTypedStore() {
213
+ const store = React.useContext(Context);
214
+ if (store === null) {
215
+ throw new Error(
216
+ "[jotai-state-tree] useStore must be used within a Provider"
217
+ );
218
+ }
219
+ return store;
220
+ }
221
+ function useTypedStoreSnapshot(selector) {
222
+ const store = useTypedStore();
223
+ const [, forceUpdate] = useState({});
224
+ useEffect(() => {
225
+ if (hasStateTreeNode(store)) {
226
+ return onSnapshot(store, () => {
227
+ forceUpdate({});
228
+ });
229
+ }
230
+ return () => {
231
+ };
232
+ }, [store]);
233
+ if (selector) {
234
+ return selector(store);
235
+ }
236
+ return store;
237
+ }
238
+ function useTypedIsAlive() {
239
+ const store = useTypedStore();
240
+ return useIsAlive(store);
241
+ }
242
+ return {
243
+ Provider: StoreProvider,
244
+ useStore: useTypedStore,
245
+ useStoreSnapshot: useTypedStoreSnapshot,
246
+ useIsAlive: useTypedIsAlive,
247
+ Context
248
+ };
249
+ }
250
+ function useSnapshot(target) {
251
+ const [snapshot, setSnapshot] = useState(() => getSnapshot(target));
252
+ useEffect(() => {
253
+ const disposer = onSnapshot(target, (newSnapshot) => {
254
+ setSnapshot(newSnapshot);
255
+ });
256
+ return disposer;
257
+ }, [target]);
258
+ return snapshot;
259
+ }
260
+ function useWatchPath(target, path, defaultValue) {
261
+ const [value, setValue] = useState(() => {
262
+ const snapshot = getSnapshot(target);
263
+ const parts = path.split(".");
264
+ let current = snapshot;
265
+ for (const part of parts) {
266
+ if (current && typeof current === "object" && part in current) {
267
+ current = current[part];
268
+ } else {
269
+ return defaultValue;
270
+ }
271
+ }
272
+ return current;
273
+ });
274
+ useEffect(() => {
275
+ const disposer = onSnapshot(target, (newSnapshot) => {
276
+ const snapshot = newSnapshot;
277
+ const parts = path.split(".");
278
+ let current = snapshot;
279
+ for (const part of parts) {
280
+ if (current && typeof current === "object" && part in current) {
281
+ current = current[part];
282
+ } else {
283
+ setValue(defaultValue);
284
+ return;
285
+ }
286
+ }
287
+ setValue(current);
288
+ });
289
+ return disposer;
290
+ }, [target, path, defaultValue]);
291
+ return value;
292
+ }
293
+ function usePatches(target, callback) {
294
+ useEffect(() => {
295
+ const { onPatch } = (init_tree(), __toCommonJS(tree_exports));
296
+ const disposer = onPatch(target, callback);
297
+ return disposer;
298
+ }, [target, callback]);
299
+ }
300
+ function useAction(action) {
301
+ return useMemo(() => action, [action]);
302
+ }
303
+ function useActions(actions) {
304
+ return useMemo(() => actions, [actions]);
305
+ }
306
+ var batchDepth = 0;
307
+ var pendingUpdates = /* @__PURE__ */ new Set();
308
+ function batch(fn) {
309
+ batchDepth++;
310
+ try {
311
+ fn();
312
+ } finally {
313
+ batchDepth--;
314
+ if (batchDepth === 0 && pendingUpdates.size > 0) {
315
+ const updates = pendingUpdates;
316
+ pendingUpdates = /* @__PURE__ */ new Set();
317
+ updates.forEach((update) => update());
318
+ }
319
+ }
320
+ }
321
+ function scheduleUpdate(update) {
322
+ if (batchDepth > 0) {
323
+ pendingUpdates.add(update);
324
+ } else {
325
+ update();
326
+ }
327
+ }
328
+ function useIsAlive(target) {
329
+ const [isAlive, setIsAlive] = useState(() => {
330
+ if (!hasStateTreeNode(target)) return false;
331
+ return getStateTreeNode(target).$isAlive;
332
+ });
333
+ useEffect(() => {
334
+ if (!hasStateTreeNode(target)) return;
335
+ const node = getStateTreeNode(target);
336
+ setIsAlive(node.$isAlive);
337
+ const disposer = onLifecycleChange(node, (alive) => {
338
+ setIsAlive(alive);
339
+ });
340
+ return disposer;
341
+ }, [target]);
342
+ return isAlive;
343
+ }
344
+ function useCleanup(cleanupFn) {
345
+ const cleanupRef = useRef(cleanupFn);
346
+ cleanupRef.current = cleanupFn;
347
+ useEffect(() => {
348
+ return () => {
349
+ cleanupRef.current();
350
+ };
351
+ }, []);
352
+ }
353
+ export {
354
+ Observer,
355
+ Provider,
356
+ batch,
357
+ createStoreContext,
358
+ observer,
359
+ scheduleUpdate,
360
+ useAction,
361
+ useActions,
362
+ useCleanup,
363
+ useIsAlive,
364
+ useLocalObservable,
365
+ useObserver,
366
+ usePatches,
367
+ useSnapshot,
368
+ useStore,
369
+ useStoreSnapshot,
370
+ useSyncedStore,
371
+ useWatchPath
372
+ };
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "jotai-state-tree",
3
+ "version": "0.1.0",
4
+ "description": "MobX-State-Tree API compatible library powered by Jotai",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ },
14
+ "./react": {
15
+ "types": "./dist/react.d.ts",
16
+ "import": "./dist/react.mjs",
17
+ "require": "./dist/react.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "src"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup src/index.ts src/react.ts --format cjs,esm --dts --clean",
26
+ "dev": "tsup src/index.ts src/react.ts --format cjs,esm --dts --watch",
27
+ "test": "vitest",
28
+ "test:run": "vitest run",
29
+ "lint": "eslint src --ext .ts,.tsx",
30
+ "typecheck": "tsc --noEmit",
31
+ "docs": "typedoc",
32
+ "docs:watch": "typedoc --watch"
33
+ },
34
+ "keywords": [
35
+ "jotai",
36
+ "mobx-state-tree",
37
+ "mst",
38
+ "state-management",
39
+ "react",
40
+ "typescript",
41
+ "state-tree"
42
+ ],
43
+ "author": "Brandon Martel",
44
+ "license": "MIT",
45
+ "peerDependencies": {
46
+ "jotai": ">=2.0.0",
47
+ "react": ">=18.0.0"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "react": {
51
+ "optional": true
52
+ }
53
+ },
54
+ "devDependencies": {
55
+ "@semantic-release/changelog": "^6.0.3",
56
+ "@semantic-release/git": "^10.0.1",
57
+ "@testing-library/jest-dom": "^6.9.1",
58
+ "@testing-library/react": "^14.3.1",
59
+ "@testing-library/user-event": "^14.6.1",
60
+ "@types/node": "^20.10.0",
61
+ "@types/react": "^18.2.0",
62
+ "eslint": "^8.55.0",
63
+ "jotai": "^2.6.0",
64
+ "jsdom": "^27.4.0",
65
+ "react": "^18.2.0",
66
+ "react-dom": "^18.3.1",
67
+ "semantic-release": "^25.0.2",
68
+ "tsup": "^8.0.0",
69
+ "typedoc": "^0.28.15",
70
+ "typescript": "^5.3.0",
71
+ "vitest": "^1.0.0"
72
+ },
73
+ "repository": {
74
+ "type": "git",
75
+ "url": "https://github.com/bmartel/jotai-state-tree"
76
+ }
77
+ }