jotai-state-tree 1.1.2 → 1.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 +30 -1150
- package/dist/{chunk-QZSMSMOP.mjs → chunk-3TQNT4MR.mjs} +258 -314
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1224 -1043
- package/dist/index.mjs +409 -180
- package/dist/react.d.mts +11 -2
- package/dist/react.d.ts +11 -2
- package/dist/react.js +377 -890
- package/dist/react.mjs +109 -87
- package/dist/{tree-CDmXgtM5.d.mts → tree-B2tSEN1S.d.mts} +6 -9
- package/dist/{tree-CDmXgtM5.d.ts → tree-B2tSEN1S.d.ts} +6 -9
- package/package.json +1 -1
- package/src/__tests__/index.test.ts +183 -0
- package/src/__tests__/memory.test.ts +48 -0
- package/src/__tests__/mst-compliance.test.ts +266 -0
- package/src/__tests__/performance.test.ts +91 -3
- package/src/__tests__/react.react.test.tsx +281 -9
- package/src/__tests__/types-inference.test.ts +280 -0
- package/src/array.ts +180 -51
- package/src/index.ts +1 -1
- package/src/lifecycle.ts +45 -10
- package/src/map.ts +21 -0
- package/src/model.ts +50 -13
- package/src/react.ts +141 -105
- package/src/tree.ts +71 -6
- package/src/types.ts +7 -7
- package/src/undo.ts +1 -3
package/README.md
CHANGED
|
@@ -2,35 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
A MobX-State-Tree (MST) compatible state management library powered by [Jotai](https://jotai.org/).
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/jotai-state-tree)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
`jotai-state-tree` combines the transactional, tree-structured state model of MobX-State-Tree with the lightweight, zero-leak, high-performance atomic updates of Jotai. It is designed to be an API-compatible, drop-in replacement for MobX-State-Tree, featuring perfect TypeScript type safety out of the box.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
5
12
|
## Features
|
|
6
13
|
|
|
7
14
|
- **MST-Compatible API** - Familiar `types.model`, `types.array`, `types.map` and more
|
|
8
|
-
- **Powered by Jotai** - Leverages Jotai's atomic state model for performance
|
|
15
|
+
- **Powered by Jotai** - Leverages Jotai's atomic state model for high performance
|
|
16
|
+
- **No Memory Leaks** - Relies on Jotai's garbage collection model (no dangling subscriptions)
|
|
9
17
|
- **Snapshots & Patches** - Full support for `getSnapshot`, `applySnapshot`, `onPatch`
|
|
10
18
|
- **Tree Navigation** - `getRoot`, `getParent`, `getPath`, `resolvePath`
|
|
11
19
|
- **References** - Type-safe references with `types.reference` and `types.safeReference`
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **Mixins** - Reusable, type-safe mixins with `types.mixin` and `.apply()`
|
|
15
|
-
- **
|
|
16
|
-
|
|
20
|
+
- **React Integration** - Fine-grained reactive observers and hooks
|
|
21
|
+
- **Zero Production Overhead** - Write protection checks are bypassed completely in production
|
|
22
|
+
- **Mixins & Composition** - Reusable, type-safe mixins with `types.mixin` and `.apply()`
|
|
23
|
+
- **Advanced Utilities** - Built-in undo managers, time travel, and action recorders
|
|
24
|
+
|
|
25
|
+
---
|
|
17
26
|
|
|
18
27
|
## Installation
|
|
19
28
|
|
|
20
29
|
```bash
|
|
21
30
|
npm install jotai-state-tree jotai
|
|
22
|
-
# or
|
|
23
|
-
yarn add jotai-state-tree jotai
|
|
24
|
-
# or
|
|
25
|
-
pnpm add jotai-state-tree jotai
|
|
26
31
|
```
|
|
27
32
|
|
|
33
|
+
---
|
|
34
|
+
|
|
28
35
|
## Quick Start
|
|
29
36
|
|
|
30
37
|
```typescript
|
|
31
|
-
import { types, getSnapshot
|
|
38
|
+
import { types, getSnapshot } from 'jotai-state-tree';
|
|
32
39
|
|
|
33
|
-
// Define your models
|
|
34
40
|
const Todo = types
|
|
35
41
|
.model('Todo', {
|
|
36
42
|
id: types.identifier,
|
|
@@ -43,1150 +49,24 @@ const Todo = types
|
|
|
43
49
|
},
|
|
44
50
|
}));
|
|
45
51
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
})
|
|
50
|
-
.views((self) => ({
|
|
51
|
-
get completedCount() {
|
|
52
|
-
return self.todos.filter((t) => t.done).length;
|
|
53
|
-
},
|
|
54
|
-
}))
|
|
55
|
-
.actions((self) => ({
|
|
56
|
-
addTodo(title: string) {
|
|
57
|
-
self.todos.push({ id: `${Date.now()}`, title });
|
|
58
|
-
},
|
|
59
|
-
}));
|
|
60
|
-
|
|
61
|
-
// Create and use
|
|
62
|
-
const store = TodoStore.create({ todos: [] });
|
|
63
|
-
store.addTodo('Learn jotai-state-tree');
|
|
64
|
-
store.todos[0].toggle();
|
|
65
|
-
console.log(getSnapshot(store));
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## Table of Contents
|
|
69
|
-
|
|
70
|
-
- [Types](#types)
|
|
71
|
-
- [Primitive Types](#primitive-types)
|
|
72
|
-
- [Identifier Types](#identifier-types)
|
|
73
|
-
- [Collection Types](#collection-types)
|
|
74
|
-
- [Optional & Nullable Types](#optional--nullable-types)
|
|
75
|
-
- [Union & Composition Types](#union--composition-types)
|
|
76
|
-
- [Reference Types](#reference-types)
|
|
77
|
-
- [Other Types](#other-types)
|
|
78
|
-
- [Models](#models)
|
|
79
|
-
- [Defining Models](#defining-models)
|
|
80
|
-
- [Views](#views)
|
|
81
|
-
- [Actions](#actions)
|
|
82
|
-
- [Volatile State](#volatile-state)
|
|
83
|
-
- [Lifecycle Hooks](#lifecycle-hooks)
|
|
84
|
-
- [Extend Method](#extend-method)
|
|
85
|
-
- [Snapshot Processing](#snapshot-processing)
|
|
86
|
-
- [Mixins](#mixins)
|
|
87
|
-
- [Model Composition](#model-composition)
|
|
88
|
-
- [Tree Utilities](#tree-utilities)
|
|
89
|
-
- [React Integration](#react-integration)
|
|
90
|
-
- [Undo/Redo & Time Travel](#undoredo--time-travel)
|
|
91
|
-
- [Model Registry](#model-registry)
|
|
92
|
-
- [Middleware](#middleware)
|
|
93
|
-
- [Flow (Async Actions)](#flow-async-actions)
|
|
94
|
-
- [Type Utilities](#type-utilities)
|
|
95
|
-
- [Migration from MST](#migration-from-mst)
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## Types
|
|
100
|
-
|
|
101
|
-
### Primitive Types
|
|
102
|
-
|
|
103
|
-
| Type | Description |
|
|
104
|
-
|------|-------------|
|
|
105
|
-
| `types.string` | String values |
|
|
106
|
-
| `types.number` | Number values (floats) |
|
|
107
|
-
| `types.integer` | Integer values only |
|
|
108
|
-
| `types.boolean` | Boolean values |
|
|
109
|
-
| `types.finite` | Finite numbers (excludes Infinity) |
|
|
110
|
-
| `types.float` | Alias for number |
|
|
111
|
-
| `types.Date` | Date objects (stored as timestamp) |
|
|
112
|
-
| `types.null` | Null values |
|
|
113
|
-
| `types.undefined` | Undefined values |
|
|
114
|
-
|
|
115
|
-
### Identifier Types
|
|
116
|
-
|
|
117
|
-
```typescript
|
|
118
|
-
const User = types.model('User', {
|
|
119
|
-
id: types.identifier, // String identifier
|
|
120
|
-
numericId: types.identifierNumber, // Number identifier
|
|
121
|
-
});
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### Collection Types
|
|
125
|
-
|
|
126
|
-
**Array Type:**
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
const TodoList = types.model('TodoList', {
|
|
130
|
-
items: types.array(Todo),
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// Array methods
|
|
134
|
-
list.items.push({ id: '1', title: 'New' });
|
|
135
|
-
list.items.replace([...]); // Replace all items
|
|
136
|
-
list.items.clear(); // Remove all items
|
|
137
|
-
list.items.remove(item); // Remove specific item
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
**Map Type:**
|
|
141
|
-
|
|
142
|
-
```typescript
|
|
143
|
-
const UserStore = types.model('UserStore', {
|
|
144
|
-
users: types.map(User),
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// Map methods
|
|
148
|
-
store.users.set('user-1', { id: 'user-1', name: 'John' });
|
|
149
|
-
store.users.put({ id: 'user-2', name: 'Jane' }); // Uses identifier as key
|
|
150
|
-
store.users.merge({ 'user-3': { id: 'user-3', name: 'Bob' } });
|
|
151
|
-
store.users.delete('user-1');
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
### Optional & Nullable Types
|
|
155
|
-
|
|
156
|
-
```typescript
|
|
157
|
-
types.optional(types.string, '') // Default value when undefined
|
|
158
|
-
types.optional(types.number, () => Date.now()) // Factory default
|
|
159
|
-
|
|
160
|
-
types.maybe(types.string) // string | undefined
|
|
161
|
-
types.maybeNull(types.string) // string | null
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
### Union & Composition Types
|
|
165
|
-
|
|
166
|
-
```typescript
|
|
167
|
-
// Union type
|
|
168
|
-
const Status = types.union(
|
|
169
|
-
types.literal('pending'),
|
|
170
|
-
types.literal('done'),
|
|
171
|
-
types.literal('error')
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
// Union with dispatcher
|
|
175
|
-
const Shape = types.union(
|
|
176
|
-
{ dispatcher: (snapshot) => snapshot.type === 'circle' ? Circle : Rectangle },
|
|
177
|
-
Circle,
|
|
178
|
-
Rectangle
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
// Late type (for recursive/circular references)
|
|
182
|
-
const TreeNode = types.model('TreeNode', {
|
|
183
|
-
value: types.string,
|
|
184
|
-
children: types.array(types.late(() => TreeNode)),
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// Refinement type
|
|
188
|
-
const PositiveNumber = types.refinement(
|
|
189
|
-
types.number,
|
|
190
|
-
(value) => value > 0,
|
|
191
|
-
'Value must be positive'
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
// Literal type
|
|
195
|
-
const Direction = types.literal('north');
|
|
196
|
-
|
|
197
|
-
// Enumeration
|
|
198
|
-
const Color = types.enumeration('Color', ['red', 'green', 'blue']);
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
### Reference Types
|
|
202
|
-
|
|
203
|
-
```typescript
|
|
204
|
-
const Author = types.model('Author', {
|
|
205
|
-
id: types.identifier,
|
|
206
|
-
name: types.string,
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const Book = types.model('Book', {
|
|
210
|
-
title: types.string,
|
|
211
|
-
author: types.reference(Author), // Throws if not found
|
|
212
|
-
editor: types.safeReference(Author), // Returns undefined if not found
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
// Custom reference options
|
|
216
|
-
const customRef = types.reference(Author, {
|
|
217
|
-
get(identifier, parent) {
|
|
218
|
-
return resolveAuthor(identifier);
|
|
219
|
-
},
|
|
220
|
-
set(author) {
|
|
221
|
-
return author.id;
|
|
222
|
-
},
|
|
223
|
-
onInvalidated({ parent, invalidId, replaceRef, removeRef, cause }) {
|
|
224
|
-
removeRef(); // or replaceRef(newAuthor)
|
|
225
|
-
},
|
|
226
|
-
});
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
### Other Types
|
|
230
|
-
|
|
231
|
-
```typescript
|
|
232
|
-
// Frozen (immutable deep objects)
|
|
233
|
-
const Config = types.model('Config', {
|
|
234
|
-
settings: types.frozen<{ theme: string; debug: boolean }>(),
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
// Custom type
|
|
238
|
-
const CustomDate = types.custom<string, Date>({
|
|
239
|
-
name: 'CustomDate',
|
|
240
|
-
fromSnapshot(value: string) { return new Date(value); },
|
|
241
|
-
toSnapshot(value: Date) { return value.toISOString(); },
|
|
242
|
-
isTargetType(value) { return value instanceof Date; },
|
|
243
|
-
getValidationMessage(value) { return 'Invalid date'; },
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
// Snapshot processor
|
|
247
|
-
const ProcessedModel = types.snapshotProcessor(BaseModel, {
|
|
248
|
-
preProcessor(snapshot) {
|
|
249
|
-
return { ...snapshot, version: snapshot.version ?? 1 };
|
|
250
|
-
},
|
|
251
|
-
postProcessor(snapshot) {
|
|
252
|
-
return { ...snapshot, exported: true };
|
|
253
|
-
},
|
|
254
|
-
});
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
---
|
|
258
|
-
|
|
259
|
-
## Models
|
|
260
|
-
|
|
261
|
-
### Defining Models
|
|
262
|
-
|
|
263
|
-
```typescript
|
|
264
|
-
const User = types.model('User', {
|
|
265
|
-
id: types.identifier,
|
|
266
|
-
name: types.string,
|
|
267
|
-
age: types.optional(types.number, 0),
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
// Anonymous model
|
|
271
|
-
const Point = types.model({
|
|
272
|
-
x: types.number,
|
|
273
|
-
y: types.number,
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
// Add properties later
|
|
277
|
-
const ExtendedUser = User.props({
|
|
278
|
-
email: types.string,
|
|
279
|
-
});
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
### Views
|
|
283
|
-
|
|
284
|
-
Views are computed properties derived from state:
|
|
285
|
-
|
|
286
|
-
```typescript
|
|
287
|
-
const User = types
|
|
288
|
-
.model('User', {
|
|
289
|
-
firstName: types.string,
|
|
290
|
-
lastName: types.string,
|
|
291
|
-
})
|
|
292
|
-
.views((self) => ({
|
|
293
|
-
// Getter view
|
|
294
|
-
get fullName() {
|
|
295
|
-
return `${self.firstName} ${self.lastName}`;
|
|
296
|
-
},
|
|
297
|
-
// Method view
|
|
298
|
-
getGreeting(prefix: string) {
|
|
299
|
-
return `${prefix} ${self.fullName}!`;
|
|
300
|
-
},
|
|
301
|
-
}));
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
### Actions
|
|
305
|
-
|
|
306
|
-
Actions are methods that modify state:
|
|
307
|
-
|
|
308
|
-
```typescript
|
|
309
|
-
const Counter = types
|
|
310
|
-
.model('Counter', {
|
|
311
|
-
count: types.optional(types.number, 0),
|
|
312
|
-
})
|
|
313
|
-
.actions((self) => ({
|
|
314
|
-
increment() {
|
|
315
|
-
self.count++;
|
|
316
|
-
},
|
|
317
|
-
decrement() {
|
|
318
|
-
self.count--;
|
|
319
|
-
},
|
|
320
|
-
setCount(value: number) {
|
|
321
|
-
self.count = value;
|
|
322
|
-
},
|
|
323
|
-
}));
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
### Volatile State
|
|
327
|
-
|
|
328
|
-
Non-serialized state that doesn't appear in snapshots:
|
|
329
|
-
|
|
330
|
-
```typescript
|
|
331
|
-
const FormModel = types
|
|
332
|
-
.model('FormModel', {
|
|
333
|
-
data: types.string,
|
|
334
|
-
})
|
|
335
|
-
.volatile(() => ({
|
|
336
|
-
isLoading: false,
|
|
337
|
-
error: null as string | null,
|
|
338
|
-
abortController: null as AbortController | null,
|
|
339
|
-
}))
|
|
340
|
-
.actions((self) => ({
|
|
341
|
-
async fetchData() {
|
|
342
|
-
self.isLoading = true;
|
|
343
|
-
self.abortController = new AbortController();
|
|
344
|
-
try {
|
|
345
|
-
const result = await fetch('/api/data', {
|
|
346
|
-
signal: self.abortController.signal
|
|
347
|
-
});
|
|
348
|
-
self.data = await result.text();
|
|
349
|
-
} catch (e) {
|
|
350
|
-
self.error = e.message;
|
|
351
|
-
} finally {
|
|
352
|
-
self.isLoading = false;
|
|
353
|
-
}
|
|
354
|
-
},
|
|
355
|
-
}));
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
### Lifecycle Hooks
|
|
359
|
-
|
|
360
|
-
```typescript
|
|
361
|
-
const Model = types
|
|
362
|
-
.model('Model', { value: types.string })
|
|
363
|
-
.afterCreate((self) => {
|
|
364
|
-
console.log('Created:', self.value);
|
|
365
|
-
})
|
|
366
|
-
.afterAttach((self) => {
|
|
367
|
-
console.log('Attached to tree');
|
|
368
|
-
})
|
|
369
|
-
.beforeDetach((self) => {
|
|
370
|
-
console.log('About to detach');
|
|
371
|
-
})
|
|
372
|
-
.beforeDestroy((self) => {
|
|
373
|
-
console.log('About to be destroyed');
|
|
374
|
-
});
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
### Extend Method
|
|
378
|
-
|
|
379
|
-
Combine views, actions, and volatile in one call with shared closure:
|
|
380
|
-
|
|
381
|
-
```typescript
|
|
382
|
-
const Counter = types
|
|
383
|
-
.model('Counter', {
|
|
384
|
-
count: types.optional(types.number, 0),
|
|
385
|
-
})
|
|
386
|
-
.extend((self) => {
|
|
387
|
-
// Private closure state
|
|
388
|
-
let lastModified = Date.now();
|
|
389
|
-
|
|
390
|
-
return {
|
|
391
|
-
views: {
|
|
392
|
-
get doubled() {
|
|
393
|
-
return self.count * 2;
|
|
394
|
-
},
|
|
395
|
-
get lastModified() {
|
|
396
|
-
return lastModified;
|
|
397
|
-
},
|
|
398
|
-
},
|
|
399
|
-
actions: {
|
|
400
|
-
increment() {
|
|
401
|
-
self.count++;
|
|
402
|
-
lastModified = Date.now();
|
|
403
|
-
},
|
|
404
|
-
},
|
|
405
|
-
state: {
|
|
406
|
-
isEditing: false,
|
|
407
|
-
},
|
|
408
|
-
};
|
|
409
|
-
});
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
### Snapshot Processing
|
|
413
|
-
|
|
414
|
-
Transform snapshots during creation and serialization:
|
|
415
|
-
|
|
416
|
-
```typescript
|
|
417
|
-
const Model = types
|
|
418
|
-
.model('Model', {
|
|
419
|
-
data: types.string,
|
|
420
|
-
version: types.number,
|
|
421
|
-
})
|
|
422
|
-
.preProcessSnapshot((snapshot) => ({
|
|
423
|
-
...snapshot,
|
|
424
|
-
version: snapshot.version ?? 1, // Add defaults
|
|
425
|
-
}))
|
|
426
|
-
.postProcessSnapshot((snapshot) => ({
|
|
427
|
-
...snapshot,
|
|
428
|
-
exportedAt: Date.now(), // Add metadata
|
|
429
|
-
}));
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
---
|
|
433
|
-
|
|
434
|
-
## Mixins
|
|
435
|
-
|
|
436
|
-
Create reusable, type-safe mixins that can be applied to models:
|
|
437
|
-
|
|
438
|
-
```typescript
|
|
439
|
-
// Define a mixin with requirements
|
|
440
|
-
const Validatable = types.mixin({
|
|
441
|
-
requires: {
|
|
442
|
-
errors: types.array(types.string),
|
|
443
|
-
},
|
|
444
|
-
views: (self) => ({
|
|
445
|
-
get isValid() {
|
|
446
|
-
return self.errors.length === 0;
|
|
447
|
-
},
|
|
448
|
-
get hasErrors() {
|
|
449
|
-
return self.errors.length > 0;
|
|
450
|
-
},
|
|
451
|
-
}),
|
|
452
|
-
actions: (self) => ({
|
|
453
|
-
addError(msg: string) {
|
|
454
|
-
self.errors.push(msg);
|
|
455
|
-
},
|
|
456
|
-
clearErrors() {
|
|
457
|
-
self.errors.clear();
|
|
458
|
-
},
|
|
459
|
-
}),
|
|
460
|
-
volatile: () => ({
|
|
461
|
-
lastValidatedAt: null as number | null,
|
|
462
|
-
}),
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
// Apply mixin to a model
|
|
466
|
-
const Form = types
|
|
467
|
-
.model('Form', {
|
|
468
|
-
name: types.string,
|
|
469
|
-
email: types.string,
|
|
470
|
-
errors: types.array(types.string),
|
|
471
|
-
})
|
|
472
|
-
.apply(Validatable);
|
|
473
|
-
|
|
474
|
-
// Now Form has isValid, hasErrors, addError, clearErrors
|
|
475
|
-
const form = Form.create({ name: '', email: '', errors: [] });
|
|
476
|
-
form.addError('Name is required');
|
|
477
|
-
console.log(form.isValid); // false
|
|
478
|
-
```
|
|
479
|
-
|
|
480
|
-
**Mixin with empty requirements:**
|
|
481
|
-
|
|
482
|
-
```typescript
|
|
483
|
-
const Loadable = types.mixin({
|
|
484
|
-
volatile: () => ({
|
|
485
|
-
isLoading: false,
|
|
486
|
-
error: null as Error | null,
|
|
487
|
-
}),
|
|
488
|
-
actions: (self) => ({
|
|
489
|
-
setLoading(loading: boolean) {
|
|
490
|
-
self.isLoading = loading;
|
|
491
|
-
},
|
|
492
|
-
setError(error: Error | null) {
|
|
493
|
-
self.error = error;
|
|
494
|
-
},
|
|
495
|
-
}),
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
// Can be applied to any model
|
|
499
|
-
const DataModel = types
|
|
500
|
-
.model('DataModel', { data: types.string })
|
|
501
|
-
.apply(Loadable);
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
**Applying multiple mixins:**
|
|
505
|
-
|
|
506
|
-
```typescript
|
|
507
|
-
const Entity = types
|
|
508
|
-
.model('Entity', {
|
|
509
|
-
id: types.identifier,
|
|
510
|
-
createdAt: types.number,
|
|
511
|
-
errors: types.array(types.string),
|
|
512
|
-
})
|
|
513
|
-
.apply(Identifiable)
|
|
514
|
-
.apply(Timestamped)
|
|
515
|
-
.apply(Validatable);
|
|
516
|
-
```
|
|
517
|
-
|
|
518
|
-
---
|
|
519
|
-
|
|
520
|
-
## Model Composition
|
|
521
|
-
|
|
522
|
-
Compose multiple models into one, merging properties, views, actions, and volatile state:
|
|
523
|
-
|
|
524
|
-
```typescript
|
|
525
|
-
const Identifiable = types
|
|
526
|
-
.model('Identifiable', {
|
|
527
|
-
id: types.identifier,
|
|
528
|
-
})
|
|
529
|
-
.views((self) => ({
|
|
530
|
-
get shortId() {
|
|
531
|
-
return self.id.substring(0, 8);
|
|
532
|
-
},
|
|
533
|
-
}));
|
|
534
|
-
|
|
535
|
-
const Timestamped = types
|
|
536
|
-
.model('Timestamped', {
|
|
537
|
-
createdAt: types.number,
|
|
538
|
-
updatedAt: types.number,
|
|
539
|
-
})
|
|
540
|
-
.actions((self) => ({
|
|
541
|
-
touch() {
|
|
542
|
-
self.updatedAt = Date.now();
|
|
543
|
-
},
|
|
544
|
-
}));
|
|
545
|
-
|
|
546
|
-
// Compose models
|
|
547
|
-
const Entity = types.compose('Entity', Identifiable, Timestamped);
|
|
548
|
-
|
|
549
|
-
// Entity has: id, createdAt, updatedAt, shortId (view), touch (action)
|
|
550
|
-
const entity = Entity.create({
|
|
551
|
-
id: 'abc123',
|
|
552
|
-
createdAt: Date.now(),
|
|
553
|
-
updatedAt: Date.now(),
|
|
554
|
-
});
|
|
52
|
+
const store = Todo.create({ id: '1', title: 'Learn jotai-state-tree' });
|
|
53
|
+
store.toggle();
|
|
54
|
+
console.log(getSnapshot(store)); // { id: '1', title: 'Learn jotai-state-tree', done: true }
|
|
555
55
|
```
|
|
556
56
|
|
|
557
57
|
---
|
|
558
58
|
|
|
559
|
-
##
|
|
560
|
-
|
|
561
|
-
### Snapshots
|
|
562
|
-
|
|
563
|
-
```typescript
|
|
564
|
-
import { getSnapshot, applySnapshot, onSnapshot } from 'jotai-state-tree';
|
|
565
|
-
|
|
566
|
-
// Get current state as plain object
|
|
567
|
-
const snapshot = getSnapshot(store);
|
|
568
|
-
|
|
569
|
-
// Apply snapshot to update state
|
|
570
|
-
applySnapshot(store, { todos: [...] });
|
|
571
|
-
|
|
572
|
-
// Subscribe to snapshot changes
|
|
573
|
-
const dispose = onSnapshot(store, (snapshot) => {
|
|
574
|
-
localStorage.setItem('store', JSON.stringify(snapshot));
|
|
575
|
-
});
|
|
576
|
-
```
|
|
577
|
-
|
|
578
|
-
### Patches
|
|
579
|
-
|
|
580
|
-
```typescript
|
|
581
|
-
import { onPatch, applyPatch, recordPatches } from 'jotai-state-tree';
|
|
582
|
-
|
|
583
|
-
// Subscribe to JSON patches
|
|
584
|
-
const dispose = onPatch(store, (patch, reversePatch) => {
|
|
585
|
-
console.log('Change:', patch);
|
|
586
|
-
// { op: 'replace', path: '/todos/0/done', value: true }
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
// Apply patches
|
|
590
|
-
applyPatch(store, { op: 'replace', path: '/count', value: 5 });
|
|
591
|
-
applyPatch(store, [patch1, patch2, patch3]); // Multiple patches
|
|
592
|
-
|
|
593
|
-
// Record patches for undo
|
|
594
|
-
const recorder = recordPatches(store);
|
|
595
|
-
store.doSomething();
|
|
596
|
-
recorder.stop();
|
|
597
|
-
recorder.undo(); // Reverts changes
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
### Tree Navigation
|
|
601
|
-
|
|
602
|
-
```typescript
|
|
603
|
-
import {
|
|
604
|
-
getRoot,
|
|
605
|
-
getParent,
|
|
606
|
-
tryGetParent,
|
|
607
|
-
hasParent,
|
|
608
|
-
getParentOfType,
|
|
609
|
-
getPath,
|
|
610
|
-
getPathParts,
|
|
611
|
-
getEnv,
|
|
612
|
-
getType,
|
|
613
|
-
getIdentifier,
|
|
614
|
-
isAlive,
|
|
615
|
-
isRoot,
|
|
616
|
-
isStateTreeNode,
|
|
617
|
-
} from 'jotai-state-tree';
|
|
618
|
-
|
|
619
|
-
// Navigation
|
|
620
|
-
const root = getRoot(todo);
|
|
621
|
-
const parent = getParent(todo);
|
|
622
|
-
const maybeParent = tryGetParent(todo); // undefined if no parent
|
|
623
|
-
const store = getParentOfType(todo, TodoStore);
|
|
624
|
-
|
|
625
|
-
// Path information
|
|
626
|
-
const path = getPath(todo); // "/todos/0"
|
|
627
|
-
const parts = getPathParts(todo); // ["todos", "0"]
|
|
628
|
-
|
|
629
|
-
// Metadata
|
|
630
|
-
const env = getEnv(todo); // Environment object
|
|
631
|
-
const type = getType(todo); // TodoModel type
|
|
632
|
-
const id = getIdentifier(todo); // "todo-1" or undefined
|
|
633
|
-
|
|
634
|
-
// Status checks
|
|
635
|
-
if (isAlive(todo)) { /* still exists */ }
|
|
636
|
-
if (isRoot(store)) { /* is root node */ }
|
|
637
|
-
if (isStateTreeNode(value)) { /* is tree node */ }
|
|
638
|
-
```
|
|
639
|
-
|
|
640
|
-
### Tree Manipulation
|
|
641
|
-
|
|
642
|
-
```typescript
|
|
643
|
-
import {
|
|
644
|
-
destroy,
|
|
645
|
-
detach,
|
|
646
|
-
clone,
|
|
647
|
-
cloneDeep,
|
|
648
|
-
walk,
|
|
649
|
-
findAll,
|
|
650
|
-
findFirst,
|
|
651
|
-
freeze,
|
|
652
|
-
isFrozen,
|
|
653
|
-
unfreeze,
|
|
654
|
-
} from 'jotai-state-tree';
|
|
655
|
-
|
|
656
|
-
// Destroy node (removes from tree)
|
|
657
|
-
destroy(todo);
|
|
658
|
-
|
|
659
|
-
// Detach from parent (keeps node alive)
|
|
660
|
-
const detached = detach(todo);
|
|
661
|
-
|
|
662
|
-
// Clone node
|
|
663
|
-
const cloned = clone(todo);
|
|
664
|
-
const deepCloned = cloneDeep(todo);
|
|
665
|
-
|
|
666
|
-
// Walk entire tree
|
|
667
|
-
walk(store, (node) => {
|
|
668
|
-
console.log(getPath(node));
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
// Find nodes
|
|
672
|
-
const allTodos = findAll(store, (node) => getType(node).name === 'Todo');
|
|
673
|
-
const firstDone = findFirst(store, (node) => node.done === true);
|
|
674
|
-
|
|
675
|
-
// Freeze/unfreeze
|
|
676
|
-
freeze(store); // Make read-only
|
|
677
|
-
isFrozen(store); // true
|
|
678
|
-
unfreeze(store); // Make writable again
|
|
679
|
-
```
|
|
680
|
-
|
|
681
|
-
### Path Resolution
|
|
682
|
-
|
|
683
|
-
```typescript
|
|
684
|
-
import {
|
|
685
|
-
resolvePath,
|
|
686
|
-
tryResolve,
|
|
687
|
-
resolveIdentifier,
|
|
688
|
-
getRelativePath,
|
|
689
|
-
isAncestor,
|
|
690
|
-
haveSameRoot,
|
|
691
|
-
} from 'jotai-state-tree';
|
|
692
|
-
|
|
693
|
-
// Resolve path
|
|
694
|
-
const todo = resolvePath(store, '/todos/0');
|
|
695
|
-
const maybeTodo = tryResolve(store, '/todos/0'); // undefined if not found
|
|
696
|
-
|
|
697
|
-
// Resolve by identifier
|
|
698
|
-
const user = resolveIdentifier(User, store, 'user-123');
|
|
699
|
-
|
|
700
|
-
// Relative paths
|
|
701
|
-
const relativePath = getRelativePath(todoA, todoB);
|
|
702
|
-
// "../../todos/1"
|
|
703
|
-
|
|
704
|
-
// Ancestry checks
|
|
705
|
-
isAncestor(store, todo); // true
|
|
706
|
-
haveSameRoot(todoA, todoB); // true
|
|
707
|
-
```
|
|
708
|
-
|
|
709
|
-
---
|
|
710
|
-
|
|
711
|
-
## React Integration
|
|
712
|
-
|
|
713
|
-
### Observer HOC
|
|
714
|
-
|
|
715
|
-
```tsx
|
|
716
|
-
import { observer } from 'jotai-state-tree/react';
|
|
717
|
-
|
|
718
|
-
const TodoList = observer(({ store }) => (
|
|
719
|
-
<ul>
|
|
720
|
-
{store.todos.map((todo) => (
|
|
721
|
-
<li key={todo.id} onClick={() => todo.toggle()}>
|
|
722
|
-
{todo.done ? '✓' : '○'} {todo.title}
|
|
723
|
-
</li>
|
|
724
|
-
))}
|
|
725
|
-
</ul>
|
|
726
|
-
));
|
|
727
|
-
```
|
|
728
|
-
|
|
729
|
-
### Observer Component
|
|
730
|
-
|
|
731
|
-
```tsx
|
|
732
|
-
import { Observer } from 'jotai-state-tree/react';
|
|
733
|
-
|
|
734
|
-
function App({ store }) {
|
|
735
|
-
return (
|
|
736
|
-
<div>
|
|
737
|
-
<Observer>
|
|
738
|
-
{() => <span>Count: {store.count}</span>}
|
|
739
|
-
</Observer>
|
|
740
|
-
</div>
|
|
741
|
-
);
|
|
742
|
-
}
|
|
743
|
-
```
|
|
744
|
-
|
|
745
|
-
### Store Context (Recommended)
|
|
746
|
-
|
|
747
|
-
```tsx
|
|
748
|
-
import { createStoreContext } from 'jotai-state-tree/react';
|
|
749
|
-
|
|
750
|
-
// Create typed context
|
|
751
|
-
const { Provider, useStore, useStoreSnapshot, useIsAlive } = createStoreContext<typeof TodoStore>();
|
|
752
|
-
|
|
753
|
-
function App() {
|
|
754
|
-
const store = TodoStore.create({ todos: [] });
|
|
755
|
-
|
|
756
|
-
return (
|
|
757
|
-
<Provider value={store}>
|
|
758
|
-
<TodoList />
|
|
759
|
-
</Provider>
|
|
760
|
-
);
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
function TodoList() {
|
|
764
|
-
const store = useStore();
|
|
765
|
-
const snapshot = useStoreSnapshot((s) => s.todos);
|
|
766
|
-
const isAlive = useIsAlive();
|
|
767
|
-
|
|
768
|
-
return (
|
|
769
|
-
<ul>
|
|
770
|
-
{store.todos.map((todo) => (
|
|
771
|
-
<TodoItem key={todo.id} todo={todo} />
|
|
772
|
-
))}
|
|
773
|
-
</ul>
|
|
774
|
-
);
|
|
775
|
-
}
|
|
776
|
-
```
|
|
777
|
-
|
|
778
|
-
### Hooks
|
|
779
|
-
|
|
780
|
-
```tsx
|
|
781
|
-
import {
|
|
782
|
-
useSnapshot,
|
|
783
|
-
useWatchPath,
|
|
784
|
-
usePatches,
|
|
785
|
-
useAction,
|
|
786
|
-
useActions,
|
|
787
|
-
useLocalObservable,
|
|
788
|
-
useObserver,
|
|
789
|
-
} from 'jotai-state-tree/react';
|
|
790
|
-
|
|
791
|
-
function Component({ store }) {
|
|
792
|
-
// Subscribe to snapshot
|
|
793
|
-
const snapshot = useSnapshot(store);
|
|
794
|
-
|
|
795
|
-
// Watch specific path
|
|
796
|
-
const count = useWatchPath(store, 'count', 0);
|
|
797
|
-
|
|
798
|
-
// Subscribe to patches
|
|
799
|
-
usePatches(store, (patch) => {
|
|
800
|
-
console.log('Change:', patch);
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
// Memoized actions
|
|
804
|
-
const increment = useAction(store.increment);
|
|
805
|
-
const { add, remove } = useActions({
|
|
806
|
-
add: store.add,
|
|
807
|
-
remove: store.remove,
|
|
808
|
-
});
|
|
809
|
-
|
|
810
|
-
// Local observable state
|
|
811
|
-
const localStore = useLocalObservable(() => ({
|
|
812
|
-
count: 0,
|
|
813
|
-
increment() { this.count++; },
|
|
814
|
-
}));
|
|
815
|
-
|
|
816
|
-
// Manual observation
|
|
817
|
-
const view = useObserver(() => (
|
|
818
|
-
<span>{store.count}</span>
|
|
819
|
-
));
|
|
820
|
-
}
|
|
821
|
-
```
|
|
822
|
-
|
|
823
|
-
### Batching Updates
|
|
824
|
-
|
|
825
|
-
```tsx
|
|
826
|
-
import { batch } from 'jotai-state-tree/react';
|
|
827
|
-
|
|
828
|
-
function handleBulkUpdate() {
|
|
829
|
-
batch(() => {
|
|
830
|
-
store.item1.update();
|
|
831
|
-
store.item2.update();
|
|
832
|
-
store.item3.update();
|
|
833
|
-
// Single re-render after all updates
|
|
834
|
-
});
|
|
835
|
-
}
|
|
836
|
-
```
|
|
837
|
-
|
|
838
|
-
---
|
|
839
|
-
|
|
840
|
-
## Undo/Redo & Time Travel
|
|
841
|
-
|
|
842
|
-
### Undo Manager
|
|
843
|
-
|
|
844
|
-
```typescript
|
|
845
|
-
import { createUndoManager } from 'jotai-state-tree';
|
|
846
|
-
|
|
847
|
-
const undoManager = createUndoManager(store, {
|
|
848
|
-
maxHistoryLength: 100,
|
|
849
|
-
groupByTime: true,
|
|
850
|
-
groupingWindow: 200, // ms
|
|
851
|
-
});
|
|
852
|
-
|
|
853
|
-
// Undo/redo
|
|
854
|
-
store.increment();
|
|
855
|
-
store.increment();
|
|
856
|
-
undoManager.undo(); // count = 1
|
|
857
|
-
undoManager.redo(); // count = 2
|
|
858
|
-
|
|
859
|
-
// Check capabilities
|
|
860
|
-
undoManager.canUndo; // boolean
|
|
861
|
-
undoManager.canRedo; // boolean
|
|
862
|
-
undoManager.undoLevels; // number
|
|
863
|
-
undoManager.redoLevels; // number
|
|
864
|
-
|
|
865
|
-
// Group changes
|
|
866
|
-
undoManager.startGroup();
|
|
867
|
-
store.increment();
|
|
868
|
-
store.increment();
|
|
869
|
-
store.increment();
|
|
870
|
-
undoManager.endGroup();
|
|
871
|
-
// All three increments undo as one
|
|
872
|
-
|
|
873
|
-
// Execute without recording
|
|
874
|
-
undoManager.withoutUndo(() => {
|
|
875
|
-
store.resetToDefaults();
|
|
876
|
-
});
|
|
877
|
-
|
|
878
|
-
// Clear history
|
|
879
|
-
undoManager.clear();
|
|
880
|
-
|
|
881
|
-
// Cleanup
|
|
882
|
-
undoManager.dispose();
|
|
883
|
-
```
|
|
884
|
-
|
|
885
|
-
### Time Travel Manager
|
|
886
|
-
|
|
887
|
-
```typescript
|
|
888
|
-
import { createTimeTravelManager } from 'jotai-state-tree';
|
|
889
|
-
|
|
890
|
-
const timeTravel = createTimeTravelManager(store, {
|
|
891
|
-
maxSnapshots: 50,
|
|
892
|
-
});
|
|
893
|
-
|
|
894
|
-
// Record snapshots manually
|
|
895
|
-
store.doSomething();
|
|
896
|
-
timeTravel.record();
|
|
897
|
-
|
|
898
|
-
store.doSomethingElse();
|
|
899
|
-
timeTravel.record();
|
|
900
|
-
|
|
901
|
-
// Navigate history
|
|
902
|
-
timeTravel.goBack();
|
|
903
|
-
timeTravel.goForward();
|
|
904
|
-
timeTravel.goTo(0); // Go to first snapshot
|
|
905
|
-
|
|
906
|
-
// Inspect
|
|
907
|
-
timeTravel.currentIndex; // Current position
|
|
908
|
-
timeTravel.snapshotCount; // Total snapshots
|
|
909
|
-
timeTravel.canGoBack;
|
|
910
|
-
timeTravel.canGoForward;
|
|
911
|
-
timeTravel.getSnapshot(2); // Get specific snapshot
|
|
912
|
-
|
|
913
|
-
// Cleanup
|
|
914
|
-
timeTravel.dispose();
|
|
915
|
-
```
|
|
916
|
-
|
|
917
|
-
### Action Recorder
|
|
918
|
-
|
|
919
|
-
```typescript
|
|
920
|
-
import { createActionRecorder } from 'jotai-state-tree';
|
|
921
|
-
|
|
922
|
-
const recorder = createActionRecorder(store);
|
|
923
|
-
|
|
924
|
-
// Record actions
|
|
925
|
-
recorder.start();
|
|
926
|
-
store.addTodo('Task 1');
|
|
927
|
-
store.addTodo('Task 2');
|
|
928
|
-
store.todos[0].toggle();
|
|
929
|
-
recorder.stop();
|
|
930
|
-
|
|
931
|
-
// Get recorded actions
|
|
932
|
-
console.log(recorder.actions);
|
|
933
|
-
// [{ name: 'addTodo', args: ['Task 1'] }, ...]
|
|
934
|
-
|
|
935
|
-
// Replay on another store
|
|
936
|
-
const newStore = TodoStore.create({ todos: [] });
|
|
937
|
-
recorder.replay(newStore);
|
|
938
|
-
|
|
939
|
-
// Export/import
|
|
940
|
-
const json = recorder.export();
|
|
941
|
-
recorder.import(json);
|
|
942
|
-
|
|
943
|
-
// Cleanup
|
|
944
|
-
recorder.dispose();
|
|
945
|
-
```
|
|
946
|
-
|
|
947
|
-
---
|
|
948
|
-
|
|
949
|
-
## Model Registry
|
|
950
|
-
|
|
951
|
-
Dynamic model registration for plugin architectures and code splitting:
|
|
952
|
-
|
|
953
|
-
```typescript
|
|
954
|
-
import {
|
|
955
|
-
registerModel,
|
|
956
|
-
unregisterModel,
|
|
957
|
-
resolveModel,
|
|
958
|
-
tryResolveModel,
|
|
959
|
-
resolveModelAsync,
|
|
960
|
-
isModelRegistered,
|
|
961
|
-
getRegisteredModelNames,
|
|
962
|
-
onModelRegistered,
|
|
963
|
-
lateModel,
|
|
964
|
-
dynamicReference,
|
|
965
|
-
safeDynamicReference,
|
|
966
|
-
} from 'jotai-state-tree';
|
|
967
|
-
|
|
968
|
-
// Register models
|
|
969
|
-
registerModel('User', UserModel, { version: '1.0' });
|
|
970
|
-
registerModel('Post', PostModel);
|
|
971
|
-
|
|
972
|
-
// Check registration
|
|
973
|
-
isModelRegistered('User'); // true
|
|
974
|
-
getRegisteredModelNames(); // ['User', 'Post']
|
|
975
|
-
|
|
976
|
-
// Resolve models
|
|
977
|
-
const User = resolveModel('User');
|
|
978
|
-
const MaybePost = tryResolveModel('Post');
|
|
979
|
-
|
|
980
|
-
// Async resolution (waits for registration)
|
|
981
|
-
const Model = await resolveModelAsync('LazyModel', 5000);
|
|
982
|
-
|
|
983
|
-
// Listen for registrations
|
|
984
|
-
const dispose = onModelRegistered((name, type, metadata) => {
|
|
985
|
-
console.log(`Model registered: ${name}`);
|
|
986
|
-
});
|
|
987
|
-
|
|
988
|
-
// Late-resolving model type
|
|
989
|
-
const Comment = types.model('Comment', {
|
|
990
|
-
author: lateModel('User'), // Resolved from registry
|
|
991
|
-
});
|
|
992
|
-
|
|
993
|
-
// Dynamic references
|
|
994
|
-
const Post = types.model('Post', {
|
|
995
|
-
author: dynamicReference('User'),
|
|
996
|
-
editor: safeDynamicReference('User'),
|
|
997
|
-
});
|
|
998
|
-
|
|
999
|
-
// Unregister
|
|
1000
|
-
unregisterModel('User');
|
|
1001
|
-
```
|
|
1002
|
-
|
|
1003
|
-
---
|
|
1004
|
-
|
|
1005
|
-
## Middleware
|
|
1006
|
-
|
|
1007
|
-
Intercept and control action execution:
|
|
1008
|
-
|
|
1009
|
-
```typescript
|
|
1010
|
-
import { addMiddleware, protect, unprotect, isProtected } from 'jotai-state-tree';
|
|
1011
|
-
|
|
1012
|
-
// Add middleware
|
|
1013
|
-
const dispose = addMiddleware(store, (call, next, abort) => {
|
|
1014
|
-
console.log(`Action: ${call.name}`, call.args);
|
|
1015
|
-
|
|
1016
|
-
// Validate
|
|
1017
|
-
if (call.name === 'delete' && !canDelete()) {
|
|
1018
|
-
return abort('Not authorized');
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
// Proceed
|
|
1022
|
-
const result = next(call);
|
|
1023
|
-
|
|
1024
|
-
console.log(`Result:`, result);
|
|
1025
|
-
return result;
|
|
1026
|
-
});
|
|
1027
|
-
|
|
1028
|
-
// Protection (prevent direct mutations)
|
|
1029
|
-
protect(store);
|
|
1030
|
-
store.count = 5; // Throws error!
|
|
1031
|
-
store.increment(); // OK - through action
|
|
1032
|
-
|
|
1033
|
-
unprotect(store);
|
|
1034
|
-
store.count = 5; // OK now
|
|
1035
|
-
|
|
1036
|
-
isProtected(store); // false
|
|
1037
|
-
```
|
|
1038
|
-
|
|
1039
|
-
### Action Tracking
|
|
1040
|
-
|
|
1041
|
-
```typescript
|
|
1042
|
-
import { onAction, recordActions, applyAction } from 'jotai-state-tree';
|
|
1043
|
-
|
|
1044
|
-
// Subscribe to actions
|
|
1045
|
-
const dispose = onAction(store, (call) => {
|
|
1046
|
-
console.log(`${call.name}(${call.args.join(', ')})`);
|
|
1047
|
-
});
|
|
1048
|
-
|
|
1049
|
-
// Record actions
|
|
1050
|
-
const recorder = recordActions(store);
|
|
1051
|
-
store.addTodo('Task 1');
|
|
1052
|
-
store.todos[0].toggle();
|
|
1053
|
-
const actions = recorder.actions;
|
|
1054
|
-
recorder.stop();
|
|
1055
|
-
|
|
1056
|
-
// Replay actions
|
|
1057
|
-
recorder.replay(anotherStore);
|
|
1058
|
-
|
|
1059
|
-
// Apply single action
|
|
1060
|
-
applyAction(store, { name: 'addTodo', args: ['New Task'] });
|
|
1061
|
-
```
|
|
1062
|
-
|
|
1063
|
-
---
|
|
1064
|
-
|
|
1065
|
-
## Flow (Async Actions)
|
|
1066
|
-
|
|
1067
|
-
```typescript
|
|
1068
|
-
import { types, flow } from 'jotai-state-tree';
|
|
1069
|
-
|
|
1070
|
-
const UserStore = types
|
|
1071
|
-
.model('UserStore', {
|
|
1072
|
-
users: types.array(User),
|
|
1073
|
-
isLoading: false,
|
|
1074
|
-
})
|
|
1075
|
-
.actions((self) => ({
|
|
1076
|
-
fetchUsers: flow(function* () {
|
|
1077
|
-
self.isLoading = true;
|
|
1078
|
-
try {
|
|
1079
|
-
const response = yield fetch('/api/users');
|
|
1080
|
-
const data = yield response.json();
|
|
1081
|
-
self.users.replace(data);
|
|
1082
|
-
} catch (error) {
|
|
1083
|
-
console.error('Failed to fetch users:', error);
|
|
1084
|
-
} finally {
|
|
1085
|
-
self.isLoading = false;
|
|
1086
|
-
}
|
|
1087
|
-
}),
|
|
1088
|
-
}));
|
|
1089
|
-
|
|
1090
|
-
// Usage
|
|
1091
|
-
await store.fetchUsers();
|
|
1092
|
-
```
|
|
1093
|
-
|
|
1094
|
-
---
|
|
1095
|
-
|
|
1096
|
-
## Type Utilities
|
|
1097
|
-
|
|
1098
|
-
### Type Extraction
|
|
1099
|
-
|
|
1100
|
-
```typescript
|
|
1101
|
-
import type {
|
|
1102
|
-
Instance,
|
|
1103
|
-
SnapshotIn,
|
|
1104
|
-
SnapshotOut,
|
|
1105
|
-
ModelSelf,
|
|
1106
|
-
} from 'jotai-state-tree';
|
|
1107
|
-
|
|
1108
|
-
const Todo = types.model('Todo', { ... }).views(...).actions(...);
|
|
1109
|
-
|
|
1110
|
-
type TodoInstance = Instance<typeof Todo>;
|
|
1111
|
-
type TodoSnapshot = SnapshotIn<typeof Todo>;
|
|
1112
|
-
type TodoOutput = SnapshotOut<typeof Todo>;
|
|
1113
|
-
type TodoSelf = ModelSelf<typeof Todo>; // Full self type with views/actions
|
|
1114
|
-
```
|
|
1115
|
-
|
|
1116
|
-
### Type Checking Functions
|
|
1117
|
-
|
|
1118
|
-
```typescript
|
|
1119
|
-
import {
|
|
1120
|
-
isType,
|
|
1121
|
-
isPrimitiveType,
|
|
1122
|
-
isModelType,
|
|
1123
|
-
isArrayType,
|
|
1124
|
-
isMapType,
|
|
1125
|
-
isReferenceType,
|
|
1126
|
-
isUnionType,
|
|
1127
|
-
isOptionalType,
|
|
1128
|
-
isLateType,
|
|
1129
|
-
isFrozenType,
|
|
1130
|
-
isLiteralType,
|
|
1131
|
-
isIdentifierType,
|
|
1132
|
-
getTypeName,
|
|
1133
|
-
typecheck,
|
|
1134
|
-
} from 'jotai-state-tree';
|
|
1135
|
-
|
|
1136
|
-
// Check type kinds
|
|
1137
|
-
isModelType(Todo); // true
|
|
1138
|
-
isArrayType(types.array(types.string)); // true
|
|
1139
|
-
getTypeName(Todo); // 'Todo'
|
|
1140
|
-
|
|
1141
|
-
// Runtime type checking
|
|
1142
|
-
typecheck(Todo, value); // Throws if invalid
|
|
1143
|
-
```
|
|
1144
|
-
|
|
1145
|
-
### Validation
|
|
1146
|
-
|
|
1147
|
-
```typescript
|
|
1148
|
-
import { isValidSnapshot, getValidationError } from 'jotai-state-tree';
|
|
1149
|
-
|
|
1150
|
-
if (isValidSnapshot(Todo, data)) {
|
|
1151
|
-
const todo = Todo.create(data);
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
const error = getValidationError(Todo, invalidData);
|
|
1155
|
-
if (error) {
|
|
1156
|
-
console.error(error);
|
|
1157
|
-
}
|
|
1158
|
-
```
|
|
1159
|
-
|
|
1160
|
-
### Casting Utilities
|
|
1161
|
-
|
|
1162
|
-
```typescript
|
|
1163
|
-
import { cast, castToSnapshot, castToReferenceSnapshot } from 'jotai-state-tree';
|
|
1164
|
-
|
|
1165
|
-
// Type casting helpers
|
|
1166
|
-
const value = cast<Todo>(unknownValue);
|
|
1167
|
-
const snapshot = castToSnapshot(todo);
|
|
1168
|
-
const refId = castToReferenceSnapshot(user); // Gets identifier
|
|
1169
|
-
```
|
|
1170
|
-
|
|
1171
|
-
---
|
|
1172
|
-
|
|
1173
|
-
## Migration from MST
|
|
1174
|
-
|
|
1175
|
-
```typescript
|
|
1176
|
-
// Before (MST)
|
|
1177
|
-
import { types } from 'mobx-state-tree';
|
|
1178
|
-
import { observer } from 'mobx-react-lite';
|
|
1179
|
-
|
|
1180
|
-
// After (jotai-state-tree)
|
|
1181
|
-
import { types } from 'jotai-state-tree';
|
|
1182
|
-
import { observer } from 'jotai-state-tree/react';
|
|
1183
|
-
```
|
|
59
|
+
## Documentation Guides
|
|
1184
60
|
|
|
1185
|
-
|
|
61
|
+
Explore our detailed, exhaustive guides to master `jotai-state-tree`:
|
|
1186
62
|
|
|
1187
|
-
1.
|
|
1188
|
-
2.
|
|
1189
|
-
3.
|
|
63
|
+
1. **[Getting Started](docs/getting-started.md)** - Installation, core architecture concepts, and a complete quickstart application.
|
|
64
|
+
2. **[Models & State](docs/models-and-state.md)** - Defining models, views, actions, protection rules, volatile states, lifecycle hooks, and snapshot processing.
|
|
65
|
+
3. **[Types & Composition](docs/types-and-composition.md)** - Exhaustive list of primitives, identifiers, collections, union types, recursive structures (`types.late`), references, composition, and mixins.
|
|
66
|
+
4. **[Tree Utilities](docs/tree-utilities.md)** - Serialization (snapshots & patches), hierarchy navigation, traversal (`walk`, `find`), and relative path resolution.
|
|
67
|
+
5. **[React Integration](docs/react-integration.md)** - Observables HOCs, typed context Providers, hooks (`useSnapshot`, `useWatchPath`), and update batching.
|
|
68
|
+
6. **[Advanced Features](docs/advanced-features.md)** - Undo/Redo managers, Time Travel, Action recorders, dynamic plugins/registry, and middleware pipelines.
|
|
69
|
+
7. **[Migration from MobX-State-Tree](docs/mst-migration.md)** - Step-by-step replacement guide, performance comparisons, and key differences.
|
|
1190
70
|
|
|
1191
71
|
---
|
|
1192
72
|
|