jotai-state-tree 1.1.1 → 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 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
+ [![npm version](https://img.shields.io/npm/v/jotai-state-tree.svg)](https://www.npmjs.com/package/jotai-state-tree)
6
+ [![license](https://img.shields.io/github/license/bmartel/jotai-state-tree.svg)](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
- - **Undo/Redo** - Built-in undo manager and time-travel debugging
13
- - **React Integration** - `observer` HOC and hooks for React
14
- - **Mixins** - Reusable, type-safe mixins with `types.mixin` and `.apply()`
15
- - **Model Registry** - Dynamic model registration and resolution
16
- - **TypeScript** - Full type safety with inference
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, applySnapshot } from 'jotai-state-tree';
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 TodoStore = types
47
- .model('TodoStore', {
48
- todos: types.array(Todo),
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
- ## Tree Utilities
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
- Most MST code works with minimal changes. Key differences:
61
+ Explore our detailed, exhaustive guides to master `jotai-state-tree`:
1186
62
 
1187
- 1. Import from `jotai-state-tree` instead of `mobx-state-tree`
1188
- 2. React bindings from `jotai-state-tree/react` instead of `mobx-react-lite`
1189
- 3. Uses Jotai atoms internally instead of MobX observables
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