r-state-tree 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +291 -69
- package/dist/index.d.ts +4 -3
- package/dist/model/ChildModelsAdministration.d.ts +1 -1
- package/dist/observables/array.d.ts +6 -3
- package/dist/observables/collection.d.ts +10 -14
- package/dist/observables/index.d.ts +2 -2
- package/dist/observables/internal/Administration.d.ts +2 -3
- package/dist/observables/internal/NodeMap.d.ts +1 -0
- package/dist/observables/internal/lookup.d.ts +3 -2
- package/dist/observables/internal/utils.d.ts +1 -1
- package/dist/observables/object.d.ts +5 -4
- package/dist/observables/preact.d.ts +6 -22
- package/dist/r-state-tree.cjs +776 -384
- package/dist/r-state-tree.js +790 -395
- package/dist/toObservableTree.d.ts +1 -0
- package/dist/types.d.ts +5 -11
- package/dist/utils.d.ts +1 -1
- package/package.json +1 -1
- package/dist/computedProxy.d.ts +0 -2
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ export default defineConfig({
|
|
|
60
60
|
- Stores: application/view state containers. Create with `createStore()`, attach with `mount()`. Compose with `@child` (single or arrays, stable via `{ key }`). React to changes with `effect`/`reaction` and store lifecycles (`storeDidMount`/`storeWillUnmount`). Update reactive `props` via `updateStore()`.
|
|
61
61
|
- Models: domain state containers. Create with `Model.create()`. Persistent via snapshots (`toSnapshot`, `applySnapshot`, `onSnapshot`, diffs via `onSnapshotDiff`). Structure with `@state`, `@child`, identifiers via `@id`, and references via `@modelRef`.
|
|
62
62
|
- Context: pass data through Store/Model trees without prop drilling using `createContext<T>()`, `[Context.provide]`, and `Context.consume(this)`. Context is reactive and can be overridden by descendants.
|
|
63
|
-
- Reactivity: powered by signals. Use
|
|
63
|
+
- Reactivity: powered by signals. Use `observable()`, `@computed`, `effect`, `reaction`, `batch`, and `untracked` for precise updates.
|
|
64
64
|
|
|
65
65
|
## Separation of concerns
|
|
66
66
|
|
|
@@ -352,118 +352,172 @@ class ListStore extends Store {
|
|
|
352
352
|
|
|
353
353
|
## Mutability rules
|
|
354
354
|
|
|
355
|
-
- Models:
|
|
356
|
-
- Stores: store fields are reactive
|
|
355
|
+
- Models: model fields are shallow-reactive, but values are not auto-wrapped. For raw `@state` arrays/objects, prefer **reassignment** (immutability) so snapshots stay up to date. If you want in-place mutation (`push`, `splice`, `set`, etc.) to trigger updates and snapshot invalidation, store an `observable()` container (or `signal()`) in `@state`.
|
|
356
|
+
- Stores: store fields are shallow-reactive. Use `observable()` containers (or `signal()`) when you want in-place mutations of nested values/collections to trigger updates.
|
|
357
357
|
|
|
358
|
-
|
|
358
|
+
### `Observable` base class
|
|
359
359
|
|
|
360
|
-
For
|
|
360
|
+
For class instances that need to be reactive, extending the `Observable` base class is the supported pattern. This ensures compatibility with ES `#private` fields and built-in brand checks because the observable is created in the base constructor, allowing derived field initializers (including `#private`) to run on the observable.
|
|
361
361
|
|
|
362
362
|
```ts
|
|
363
|
-
import { Observable,
|
|
363
|
+
import { Observable, effect } from "r-state-tree";
|
|
364
364
|
|
|
365
365
|
class Counter extends Observable {
|
|
366
|
-
|
|
367
|
-
|
|
366
|
+
count = 0; // Public fields are automatically reactive
|
|
367
|
+
#internal = 0; // Private fields also work perfectly
|
|
368
368
|
|
|
369
|
-
|
|
370
|
-
return this.count
|
|
369
|
+
get total() {
|
|
370
|
+
return this.count + this.#internal;
|
|
371
371
|
}
|
|
372
372
|
|
|
373
373
|
increment() {
|
|
374
|
-
this.count
|
|
374
|
+
this.count++;
|
|
375
|
+
this.#internal++;
|
|
375
376
|
}
|
|
376
377
|
}
|
|
377
378
|
|
|
378
379
|
const counter = new Counter();
|
|
379
380
|
|
|
381
|
+
// Use an effect to track and react to property changes
|
|
380
382
|
effect(() => {
|
|
381
|
-
console.log(
|
|
383
|
+
console.log(`Visible: ${counter.count}, Total: ${counter.total}`);
|
|
382
384
|
});
|
|
383
385
|
|
|
384
|
-
counter.increment();
|
|
386
|
+
counter.increment();
|
|
385
387
|
```
|
|
386
388
|
|
|
387
|
-
|
|
389
|
+
> [!IMPORTANT] > **Why `extends Observable`?**
|
|
390
|
+
> ES `#private` fields are brand-checked. If you wrap a class instance with `observable()` _after_ it has been created (post-hoc wrapping), or if you return an observable from a standard constructor, the private state is installed on the original `this`, but methods run with the observable as `this`, causing `TypeError: Cannot read private member`.
|
|
391
|
+
>
|
|
392
|
+
> `extends Observable` solves this by returning the observable from `super()`, so derived classes initialize their private fields directly on the observable receiver.
|
|
388
393
|
|
|
389
|
-
###
|
|
394
|
+
### Limitations of `observable(instance)`
|
|
390
395
|
|
|
391
|
-
|
|
396
|
+
The `observable()` function is selective about what it makes observable. It wraps **supported containers** only:
|
|
397
|
+
|
|
398
|
+
- Plain objects
|
|
399
|
+
- Arrays
|
|
400
|
+
- Maps and Sets
|
|
401
|
+
- Dates
|
|
402
|
+
|
|
403
|
+
**Functions are not observable containers**. Calling `observable(fn)` returns the function unchanged with a dev-mode warning. However, functions stored as properties on observable objects are still automatically **batched as actions** when called—this existing behavior is preserved, just not via `observable(fn)` directly.
|
|
404
|
+
|
|
405
|
+
If you pass an arbitrary class instance, built-in (like `URL`, `RegExp`, `Promise`, or DOM objects), or frozen object to `observable()`, it will **return the object unchanged** and emit a warning in development mode.
|
|
406
|
+
|
|
407
|
+
This design prevents “silent failure” where an observable object appears to work but throws `TypeError` when accessing private members or internal slots. For your own classes, use `extends Observable`. For third-party or built-in objects, use composition:
|
|
392
408
|
|
|
393
409
|
```ts
|
|
394
|
-
|
|
410
|
+
// ❌ Post-hoc wrapping - returns raw URL, not an observable
|
|
411
|
+
const url = observable(new URL("..."));
|
|
395
412
|
|
|
396
|
-
|
|
413
|
+
// ✅ Composition - wrap a container instead
|
|
414
|
+
const state = observable({ url: new URL("...") });
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Wrap values with `observable()` for reactivity. Collections (arrays, maps, sets) track mutations:
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
import { observable, effect, isObservable } from "r-state-tree";
|
|
421
|
+
|
|
422
|
+
class DataStore {
|
|
423
|
+
// Wrap state with observable() for reactivity
|
|
424
|
+
state = observable({ count: 0 });
|
|
425
|
+
|
|
426
|
+
// Wrap array with observable() to track push/pop/splice etc.
|
|
427
|
+
items = observable([]);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const store = new DataStore();
|
|
397
431
|
|
|
398
432
|
effect(() => {
|
|
399
|
-
console.log(
|
|
433
|
+
console.log("Items length:", store.items.length);
|
|
400
434
|
});
|
|
401
435
|
|
|
402
|
-
|
|
436
|
+
store.items.push({ value: 1 }); // Triggers effect
|
|
437
|
+
console.log(isObservable(store.items[0])); // false - shallow by default
|
|
403
438
|
```
|
|
404
439
|
|
|
405
|
-
### Shallow
|
|
440
|
+
### Shallow behavior
|
|
406
441
|
|
|
407
|
-
|
|
442
|
+
All observables are **shallow by default**. Only the container's own properties are tracked—nested values are NOT wrapped:
|
|
408
443
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
| `@observable.shallow` | ✅ Yes | ❌ No | mutations + assignment |
|
|
413
|
-
| `@observable.signal` | ❌ No | ❌ No | assignment only |
|
|
444
|
+
- Collections (Arrays, Maps, Sets) wrapped with `observable()` track mutations
|
|
445
|
+
- Plain objects assigned to properties are NOT wrapped (helps preserve `structuredClone` compatibility for stored values)
|
|
446
|
+
- Nested object properties do NOT trigger effects unless explicitly wrapped
|
|
414
447
|
|
|
415
|
-
|
|
416
|
-
import { Observable, observable, effect, isObservable } from "r-state-tree";
|
|
448
|
+
**Mental model: reactive property, explicit reactive value**
|
|
417
449
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
@observable deepItems: { value: number }[] = [];
|
|
450
|
+
- Reading/writing a property on an observable container (including Stores/Models) is reactive.
|
|
451
|
+
- The _value_ you store is not auto-wrapped. If you store a plain object/array, mutating inside it won’t trigger reactions; wrap nested values with `observable()` (or use `signal()`), or use `toObservableTree` for a one-time deep wrap of an existing JSON-like structure.
|
|
421
452
|
|
|
422
|
-
|
|
423
|
-
@observable.shallow shallowItems: { value: number }[] = [];
|
|
453
|
+
**What gets tracked in shallow mode:**
|
|
424
454
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
455
|
+
| Expression | Tracked? | Why |
|
|
456
|
+
| ----------------------- | ------------- | ------------------------------------------------ |
|
|
457
|
+
| `data.nested` | ✅ Yes | Property access on the observable container |
|
|
458
|
+
| `data.nested.value` | ❌ No | `data.nested` is a plain object, not observable |
|
|
459
|
+
| `data.nested = { ... }` | ✅ Triggers | Reassigns a property on the observable container |
|
|
460
|
+
| `data.nested.value = 2` | ❌ No trigger | Mutates a plain object; container unchanged |
|
|
428
461
|
|
|
429
|
-
|
|
462
|
+
```ts
|
|
463
|
+
const data = observable({ nested: { value: 1 } });
|
|
464
|
+
|
|
465
|
+
effect(() => {
|
|
466
|
+
data.nested.value; // Reads `data.nested` (tracked), then reads `.value` (not tracked)
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
data.nested.value = 2; // Does NOT trigger — mutating a plain object
|
|
470
|
+
data.nested = { value: 3 }; // DOES trigger — reassigning a property on the observable
|
|
471
|
+
```
|
|
430
472
|
|
|
431
|
-
|
|
432
|
-
store.shallowItems.push({ value: 1 });
|
|
433
|
-
console.log(isObservable(store.shallowItems[0])); // false
|
|
473
|
+
To make `nested` reactive, wrap it explicitly:
|
|
434
474
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
store.signalItems = [1, 2]; // DOES trigger effect
|
|
475
|
+
```ts
|
|
476
|
+
const data = observable({ nested: observable({ value: 1 }) });
|
|
477
|
+
data.nested.value = 2; // Now triggers — `nested` is also observable
|
|
439
478
|
```
|
|
440
479
|
|
|
441
|
-
|
|
442
|
-
- `@observable.shallow`: Store external data that may need `structuredClone()`, or when you want change detection on the container but not deep reactivity
|
|
443
|
-
- `@observable.signal`: Maximum performance when you're replacing values rather than mutating them
|
|
480
|
+
### Plain objects (implicit reactivity)
|
|
444
481
|
|
|
445
|
-
|
|
482
|
+
Plain objects wrapped with `observable()` use implicit reactivity:
|
|
446
483
|
|
|
447
484
|
```ts
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
485
|
+
import { observable, effect } from "r-state-tree";
|
|
486
|
+
|
|
487
|
+
const state = observable({ count: 0 });
|
|
488
|
+
|
|
489
|
+
effect(() => {
|
|
490
|
+
console.log(state.count); // All properties are reactive
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
state.count++; // Triggers the effect
|
|
452
494
|
```
|
|
453
495
|
|
|
454
496
|
## Observables (low‑level)
|
|
455
497
|
|
|
456
498
|
Create reactive structures outside Stores/Models. Supported: Objects, Arrays, Map, Set, WeakMap, WeakSet.
|
|
457
499
|
|
|
458
|
-
- Track reads with `effect`/`reaction`.
|
|
459
|
-
- Access
|
|
500
|
+
- Track reads with `effect`/`reaction`. Observables are **shallow**: reads are tracked on the observable container, but nested object mutations do **not** trigger unless you explicitly wrap nested values with `observable()` (or use signals).
|
|
501
|
+
- Access backing values via `source(value)`; check if something is reactive with `isObservable(value)`.
|
|
502
|
+
- Rule of thumb: `source(...)` returns the backing data, **not** a sanitizer—it's only observable-free if you didn't manually seed observables into the backing source.
|
|
460
503
|
- Arrays: reading specific indices (`arr[i]`) or `length` tracks those; common mutators (`push/pop/shift/unshift/splice/reverse/sort/fill`) are reactive; non-index and symbol keys are not reactive.
|
|
461
504
|
|
|
505
|
+
### `source()` and structuredClone
|
|
506
|
+
|
|
507
|
+
`source(x)` returns the backing value behind an observable. It is **not** a “observable stripper”.
|
|
508
|
+
|
|
509
|
+
**One-way rule (important):**
|
|
510
|
+
|
|
511
|
+
- If you mutate through the observable wrapper (e.g. `obj.prop = observable(child)`), r-state-tree stores the **raw backing value** in `source(obj).prop` (unwrap-on-write).
|
|
512
|
+
- If you manually mutate backing sources yourself (e.g. `source(obj).prop = observable(child)`), r-state-tree does **not** sanitize or rewrite your data. In that case, `source(...)` may contain observables.
|
|
513
|
+
|
|
514
|
+
If you need to pass values to APIs that require cloneable data (e.g. `structuredClone`, `postMessage`), avoid seeding observables into backing sources. Prefer reading via `source(...)` and keep your stored values plain/cloneable.
|
|
515
|
+
|
|
462
516
|
```ts
|
|
463
517
|
import { observable, effect, computed, reaction } from "r-state-tree";
|
|
464
518
|
|
|
465
519
|
// Object
|
|
466
|
-
const state = observable({ count: 0, nested: { value: 1 } });
|
|
520
|
+
const state = observable({ count: 0, nested: observable({ value: 1 }) });
|
|
467
521
|
|
|
468
522
|
effect(() => {
|
|
469
523
|
// tracks reads
|
|
@@ -506,10 +560,86 @@ reaction(
|
|
|
506
560
|
state.count++;
|
|
507
561
|
```
|
|
508
562
|
|
|
563
|
+
### Derived collections and identity preservation
|
|
564
|
+
|
|
565
|
+
Native methods that return copies of a collection—such as `Array.prototype.slice`, `filter`, and `concat`, or `Set.prototype.union` and `intersection`—behave according to the **Explicit Architecture**:
|
|
566
|
+
|
|
567
|
+
- **Raw Return**: The returned container is a plain JavaScript object (raw, non-observable).
|
|
568
|
+
- **Identity Preservation**: Each element in the returned container maintains the same identity it had in the observable source. If an element was an observable (because it was explicitly owned), it remains an observable in the raw result.
|
|
569
|
+
|
|
570
|
+
This ensures that "derived" state is not automatically made reactive, while still allowing observers to maintain reference stability with existing objects in your state tree.
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
const item = observable({ id: 1 });
|
|
574
|
+
const arr = observable([item, { id: 2 }]);
|
|
575
|
+
|
|
576
|
+
// slice() returns a plain array
|
|
577
|
+
const sliced = arr.slice();
|
|
578
|
+
isObservable(sliced); // false
|
|
579
|
+
|
|
580
|
+
// Identity is preserved
|
|
581
|
+
sliced[0] === item; // true (same observable identity)
|
|
582
|
+
|
|
583
|
+
// If you want the result to be reactive, wrap it explicitly
|
|
584
|
+
const reactiveSlice = observable(arr.slice());
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Recursively wrapping nested values (`toObservableTree`)
|
|
588
|
+
|
|
589
|
+
By default, `observable()` is **shallow**: only the top-level container is wrapped, and nested objects/arrays are not. This is intentional for performance and `structuredClone` compatibility.
|
|
590
|
+
|
|
591
|
+
`toObservableTree` performs a **one-time initial pass** that wraps all existing nested plain objects and arrays with `observable()`. After the initial wrap, the returned observables behave exactly like normal shallow observables — new assignments are **not** auto-wrapped.
|
|
592
|
+
|
|
593
|
+
```ts
|
|
594
|
+
import { toObservableTree, effect, source, isObservable } from "r-state-tree";
|
|
595
|
+
|
|
596
|
+
// Initial pass wraps all existing nested plain objects/arrays
|
|
597
|
+
const tree = toObservableTree({
|
|
598
|
+
user: { name: "Alice", tags: ["admin", "active"] },
|
|
599
|
+
settings: { theme: "dark" },
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
effect(() => {
|
|
603
|
+
// Existing nested values are observable and tracked
|
|
604
|
+
console.log(tree.user.name);
|
|
605
|
+
console.log(tree.user.tags[0]);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// Mutations to initially-wrapped values trigger effects
|
|
609
|
+
tree.user.name = "Bob"; // triggers
|
|
610
|
+
tree.user.tags[0] = "superadmin"; // triggers
|
|
611
|
+
|
|
612
|
+
// NEW assignments are NOT auto-wrapped (normal shallow behavior)
|
|
613
|
+
tree.newProp = { foo: 1 };
|
|
614
|
+
isObservable(tree.newProp); // false — not wrapped
|
|
615
|
+
|
|
616
|
+
// Source is observable-free and clonable *as long as you don't manually seed observables*
|
|
617
|
+
// into backing data structures. r-state-tree unwraps observables on writes performed
|
|
618
|
+
// through observable containers, but it does not sanitize user-mutated backing sources.
|
|
619
|
+
const snapshot = structuredClone(source(tree));
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
**Key behavior:**
|
|
623
|
+
|
|
624
|
+
- **One-time pass**: Only values present at call time are wrapped. New assignments afterward behave like normal `observable()` (shallow).
|
|
625
|
+
- **Not MobX-style "deep"**: This does NOT change the observable's behavior. It's just a convenience for wrapping an existing structure upfront.
|
|
626
|
+
|
|
627
|
+
**When to use:**
|
|
628
|
+
|
|
629
|
+
- Hydrating API/JSON responses where you want all nested values observable from the start
|
|
630
|
+
- Cases where manually wrapping each nested object would be tedious
|
|
631
|
+
|
|
632
|
+
**Constraints:**
|
|
633
|
+
|
|
634
|
+
- Input must be JSON-like and **acyclic** (no circular references)
|
|
635
|
+
- Only plain objects and arrays are wrapped; other types (Map, Set, Date, class instances, RegExp, Error, etc.) are left as-is and not traversed
|
|
636
|
+
|
|
509
637
|
## Models and snapshots
|
|
510
638
|
|
|
511
639
|
Models capture persistent state with snapshot utilities.
|
|
512
640
|
|
|
641
|
+
**Important:** `@state` is **shallow-reactive** at the property level (assignments track), but values are **not auto-wrapped**. If you need nested mutations to be reactive (and to invalidate snapshot caches on in-place mutation), store `observable()` containers or `signal()` values inside `@state`.
|
|
642
|
+
|
|
513
643
|
```ts
|
|
514
644
|
import {
|
|
515
645
|
Model,
|
|
@@ -536,6 +666,94 @@ todo.title = "Learn r-state-tree";
|
|
|
536
666
|
stop();
|
|
537
667
|
```
|
|
538
668
|
|
|
669
|
+
### Snapshot data contract
|
|
670
|
+
|
|
671
|
+
**Snapshots are JSON-only**: they contain primitives, arrays, plain objects, and Dates (serialized as ISO strings).
|
|
672
|
+
|
|
673
|
+
- **Primitives**: `string`, `number`, `boolean`, `null`, `undefined` pass through.
|
|
674
|
+
- **Arrays**: recursively cloned.
|
|
675
|
+
- **Plain objects**: recursively cloned (prototype must be `Object.prototype` or `null`).
|
|
676
|
+
- **Dates**: serialize to ISO strings (e.g., `"2024-01-15T10:30:00.000Z"`).
|
|
677
|
+
- **Signals**: serialize to their current `.value` (recursively cloned).
|
|
678
|
+
- **Map/Set/WeakMap/WeakSet and other non-plain objects are rejected** with a descriptive error. Convert them to plain structures before storing in `@state`.
|
|
679
|
+
|
|
680
|
+
```ts
|
|
681
|
+
class Event extends Model {
|
|
682
|
+
@state title = "Meeting";
|
|
683
|
+
@state createdAt = new Date(); // Date → ISO string in snapshot
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const event = Event.create();
|
|
687
|
+
toSnapshot(event);
|
|
688
|
+
// { title: "Meeting", createdAt: "2024-01-15T10:30:00.000Z" }
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
If you store a `Map` or class instance in `@state`, snapshotting will throw:
|
|
692
|
+
|
|
693
|
+
```ts
|
|
694
|
+
class M extends Model {
|
|
695
|
+
@state cache = new Map(); // ❌ Will throw on toSnapshot()
|
|
696
|
+
}
|
|
697
|
+
// Error: r-state-tree: snapshots do not support Map at path "cache". ...
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
Convert to a plain structure instead:
|
|
701
|
+
|
|
702
|
+
```ts
|
|
703
|
+
class M extends Model {
|
|
704
|
+
@state cache: Record<string, unknown> = {}; // ✅ Plain object
|
|
705
|
+
}
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
### Snapshot invalidation rules
|
|
709
|
+
|
|
710
|
+
Snapshots are **memoized computeds**. Once a snapshot is observed (via `onSnapshot`, `onSnapshotDiff`, or `toSnapshot`), subsequent calls return the cached value unless a reactive dependency changes.
|
|
711
|
+
|
|
712
|
+
Because snapshots are **memoized computeds** and observables are **shallow**, the snapshot cache is invalidated only by:
|
|
713
|
+
|
|
714
|
+
1. **Reassigning** the `@state` field itself.
|
|
715
|
+
2. **Mutating observable containers** (`observable()`) or **signals** (`signal()`) stored in the field.
|
|
716
|
+
|
|
717
|
+
**Rule:** Treat raw `@state` values (plain objects/arrays) as immutable. If you mutate them in place without reassignment, the snapshot cache goes stale—`onSnapshot` won't fire and `toSnapshot` returns the old cached value.
|
|
718
|
+
|
|
719
|
+
```ts
|
|
720
|
+
class M extends Model {
|
|
721
|
+
@state tags: string[] = [];
|
|
722
|
+
|
|
723
|
+
// ❌ In-place mutation — snapshot cache goes stale
|
|
724
|
+
addTagBroken(tag: string) {
|
|
725
|
+
this.tags.push(tag);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// ✅ Reassign — invalidates cache, onSnapshot fires
|
|
729
|
+
addTagReassign(tag: string) {
|
|
730
|
+
this.tags = [...this.tags, tag];
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
If you need in-place mutations **and** snapshot updates, wrap the value in `observable()` or use `signal()`:
|
|
736
|
+
|
|
737
|
+
```ts
|
|
738
|
+
class M extends Model {
|
|
739
|
+
// ✅ observable() container — in-place mutations invalidate cache
|
|
740
|
+
@state items: { id: number }[] = observable([]);
|
|
741
|
+
|
|
742
|
+
addItem(id: number) {
|
|
743
|
+
this.items.push({ id }); // onSnapshot fires
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
class Counter extends Model {
|
|
748
|
+
// ✅ signal() — .value updates invalidate cache
|
|
749
|
+
@state count = signal(0);
|
|
750
|
+
|
|
751
|
+
increment() {
|
|
752
|
+
this.count.value++; // onSnapshot fires
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
```
|
|
756
|
+
|
|
539
757
|
### Snapshots and persistence
|
|
540
758
|
|
|
541
759
|
- Snapshots capture Models (not Stores).
|
|
@@ -685,8 +903,10 @@ class ContainerModel extends Model {
|
|
|
685
903
|
- Context
|
|
686
904
|
- `createContext`, type `Context`
|
|
687
905
|
- Reactivity and observables
|
|
688
|
-
- `observable`, `computed`, `effect`, `reaction`, `batch`, `untracked
|
|
906
|
+
- `observable`, `computed`, `effect`, `reaction`, `batch`, `untracked`
|
|
689
907
|
- Utilities: `isObservable`, `source`, `reportObserved`, `reportChanged`
|
|
908
|
+
- Advanced
|
|
909
|
+
- `toObservableTree` — recursively wrap nested values in a JSON-like structure
|
|
690
910
|
- Signals interop
|
|
691
911
|
- `signal`, `getSignal`, types `Signal`, `ReadonlySignal`
|
|
692
912
|
|
|
@@ -694,7 +914,7 @@ class ContainerModel extends Model {
|
|
|
694
914
|
|
|
695
915
|
r-state-tree is built on `@preact/signals-core`. You can interoperate with signals directly:
|
|
696
916
|
|
|
697
|
-
- Per-property signals via
|
|
917
|
+
- Per-property signals via `getSignal(obj, key)`.
|
|
698
918
|
- Re-exported utilities: `signal`, `computed`, `effect`, `batch`, `untracked`, and types `Signal`, `ReadonlySignal`.
|
|
699
919
|
|
|
700
920
|
```ts
|
|
@@ -702,17 +922,14 @@ import { observable, effect, getSignal } from "r-state-tree";
|
|
|
702
922
|
|
|
703
923
|
const state = observable({ count: 0 });
|
|
704
924
|
|
|
705
|
-
|
|
706
|
-
const s1 = state.$count;
|
|
707
|
-
const s2 = getSignal(state, "count");
|
|
925
|
+
const countSignal = getSignal(state, "count");
|
|
708
926
|
|
|
709
927
|
effect(() => {
|
|
710
|
-
|
|
711
|
-
console.log("count:", s1.value);
|
|
928
|
+
console.log("count:", countSignal.value);
|
|
712
929
|
});
|
|
713
930
|
|
|
714
931
|
// Update via signal or through the object
|
|
715
|
-
|
|
932
|
+
countSignal.value = 1;
|
|
716
933
|
state.count = 2;
|
|
717
934
|
```
|
|
718
935
|
|
|
@@ -724,18 +941,20 @@ state.count = 2;
|
|
|
724
941
|
```ts
|
|
725
942
|
// Preact
|
|
726
943
|
function TodoView({ store }: { store: TodoStore }) {
|
|
727
|
-
|
|
944
|
+
const titleSignal = getSignal(store, "title");
|
|
945
|
+
return <h1>{titleSignal.value}</h1>;
|
|
728
946
|
}
|
|
729
947
|
|
|
730
948
|
// React
|
|
731
949
|
import { useSignals } from "@preact/signals-react/runtime";
|
|
732
950
|
function TodoView({ store }: { store: TodoStore }) {
|
|
733
951
|
useSignals();
|
|
734
|
-
|
|
952
|
+
const titleSignal = getSignal(store, "title");
|
|
953
|
+
return <h1>{titleSignal.value}</h1>;
|
|
735
954
|
}
|
|
736
955
|
```
|
|
737
956
|
|
|
738
|
-
|
|
957
|
+
Use the observers/renderers provided by the signals bindings for your UI library; r-state-tree will participate automatically because Stores/Models are signal-backed.
|
|
739
958
|
|
|
740
959
|
### Identifier and reference rules
|
|
741
960
|
|
|
@@ -798,7 +1017,8 @@ Do:
|
|
|
798
1017
|
- Delegate from Stores to Models for domain changes
|
|
799
1018
|
- Use `@child` for child stores (getter-based)
|
|
800
1019
|
- Use stable `key` values for child stores
|
|
801
|
-
- Mutate model
|
|
1020
|
+
- Mutate **`@child` model collections** and **`observable()` containers** in place (push/splice/set/add/etc.)
|
|
1021
|
+
- Treat **raw `@state` arrays/objects** as immutable: use reassignment so snapshots stay up to date (or store an `observable()` container / `signal()` inside `@state`)
|
|
802
1022
|
|
|
803
1023
|
Don’t:
|
|
804
1024
|
|
|
@@ -884,7 +1104,9 @@ const off = onSnapshot(m, (snap) =>
|
|
|
884
1104
|
## Common pitfalls
|
|
885
1105
|
|
|
886
1106
|
- Forgetting stable keys for `@child` arrays causes identity churn.
|
|
887
|
-
- Assuming deep reactivity
|
|
1107
|
+
- Assuming **deep** reactivity: nested plain objects/arrays are not reactive unless you explicitly wrap them (or use `toObservableTree` for initial hydration).
|
|
1108
|
+
- Mutating raw `@state` arrays/objects in place (`push`, `obj.x = 1`) and expecting snapshots to update. Snapshots are memoized; use reassignment or store `observable()` containers / `signal()` values in `@state`.
|
|
1109
|
+
- Passing observables to third‑party APIs that expect cloneable/serializable values (e.g. `structuredClone`). Use `source(value)` to get the backing value. It will be observable-free for values written via r-state-tree’s observable APIs (unwrap-on-write), but `source(...)` is **not** guaranteed observable-free if you manually seed observables into backing sources.
|
|
888
1110
|
- Creating child stores in constructors: `@child` must be on getters so identity and lifecycle can be managed by the framework.
|
|
889
1111
|
- Passing `models` into child stores during mount can create a recursive mount loop. If a child needs parent models, create the child store/model inside `storeDidMount` instead of wiring it through `models` during the mount cycle.
|
|
890
1112
|
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,9 @@ import Store, { createStore, updateStore } from "./store/Store";
|
|
|
3
3
|
import Model from "./model/Model";
|
|
4
4
|
import { mount, unmount, onSnapshot, toSnapshot, applySnapshot, onSnapshotDiff } from "./api";
|
|
5
5
|
import { createContext } from "./context";
|
|
6
|
-
|
|
7
|
-
export {
|
|
6
|
+
import { toObservableTree } from "./toObservableTree";
|
|
7
|
+
export { createStore, Store, Model, mount, unmount, updateStore, onSnapshot, onSnapshotDiff, toSnapshot, applySnapshot, createContext, toObservableTree, };
|
|
8
|
+
export { observable, computed, source, reportChanged, reportObserved, getSignal, isObservable, reaction, Signal, type ReadonlySignal, batch, untracked, effect, signal, Observable, } from "./observables";
|
|
8
9
|
export * from "./decorators";
|
|
9
|
-
export type { Configuration, Snapshot, IdType, SnapshotDiff } from "./types";
|
|
10
|
+
export type { Configuration, Snapshot, SnapshotValue, IdType, SnapshotDiff, } from "./types";
|
|
10
11
|
export type { Context } from "./context";
|
|
@@ -29,7 +29,7 @@ declare class ObservableListener<T> {
|
|
|
29
29
|
notify(ev: MutationEvent<T>): void;
|
|
30
30
|
}
|
|
31
31
|
export declare class ChildModelsAdministration<T> extends ArrayAdministration<T> {
|
|
32
|
-
set(index: number, newValue: T):
|
|
32
|
+
set(index: number, newValue: T): boolean;
|
|
33
33
|
spliceWithArray(index: number, deleteCount?: number | undefined, newItems?: T[] | undefined): T[];
|
|
34
34
|
}
|
|
35
35
|
export {};
|
|
@@ -4,17 +4,20 @@ import { SignalMap } from "./internal/NodeMap";
|
|
|
4
4
|
export declare class ArrayAdministration<T> extends Administration<T[]> {
|
|
5
5
|
valuesMap: SignalMap<number>;
|
|
6
6
|
keysAtom: AtomNode;
|
|
7
|
+
private observableSources;
|
|
8
|
+
private rawSources;
|
|
9
|
+
private trackValue;
|
|
7
10
|
static proxyTraps: ProxyHandler<Array<unknown>>;
|
|
8
11
|
static methods: Partial<{
|
|
9
12
|
[K in keyof typeof Array.prototype as (typeof Array.prototype)[K] extends Function ? K : never]: (typeof Array.prototype)[K];
|
|
10
13
|
}>;
|
|
11
14
|
constructor(source?: T[]);
|
|
12
|
-
protected reportObserveDeep(): void;
|
|
13
15
|
getNode(key?: number): unknown;
|
|
14
16
|
get(index: number): T | undefined;
|
|
15
|
-
|
|
17
|
+
private _getEffectiveValue;
|
|
18
|
+
set(index: number, newValue: T): boolean;
|
|
16
19
|
getArrayLength(): number;
|
|
17
|
-
setArrayLength(
|
|
20
|
+
setArrayLength(input: unknown): boolean;
|
|
18
21
|
spliceWithArray(index: number, deleteCount?: number, newItems?: T[]): T[];
|
|
19
22
|
spliceItemsIntoValues(index: number, deleteCount: number, newItems: T[]): T[];
|
|
20
23
|
onArrayChanged(lengthChanged?: boolean, index?: number, count?: number): void;
|
|
@@ -7,30 +7,26 @@ export declare class CollectionAdministration<K, V = K> extends Administration<C
|
|
|
7
7
|
hasMap: AtomMap<K>;
|
|
8
8
|
valuesMap: SignalMap<K>;
|
|
9
9
|
keysAtom: AtomNode;
|
|
10
|
+
private isWeak;
|
|
11
|
+
private strongTracking;
|
|
12
|
+
private weakTracking;
|
|
10
13
|
static proxyTraps: ProxyHandler<Set<unknown> | Map<unknown, unknown>>;
|
|
11
|
-
static methods:
|
|
12
|
-
clear: (this: any) => unknown;
|
|
13
|
-
forEach: (this: any) => unknown;
|
|
14
|
-
has: (this: any) => unknown;
|
|
15
|
-
add: (this: any) => unknown;
|
|
16
|
-
set: (this: any) => unknown;
|
|
17
|
-
get: (this: any) => unknown;
|
|
18
|
-
delete: (this: any) => unknown;
|
|
19
|
-
entries: (this: any) => unknown;
|
|
20
|
-
keys: (this: any) => unknown;
|
|
21
|
-
values: (this: any) => unknown;
|
|
22
|
-
[Symbol.iterator]: (this: any) => unknown;
|
|
23
|
-
};
|
|
14
|
+
static methods: Record<PropertyKey, Function>;
|
|
24
15
|
constructor(source: Collection<K, V>);
|
|
16
|
+
private trackExplicitObservable;
|
|
17
|
+
private untrackExplicitObservable;
|
|
18
|
+
private hasExplicitObservable;
|
|
19
|
+
private getProxyVariant;
|
|
20
|
+
private getExistingKey;
|
|
25
21
|
private hasEntry;
|
|
26
22
|
private onCollectionChange;
|
|
27
|
-
protected reportObserveDeep(): void;
|
|
28
23
|
getNode(key?: K): unknown;
|
|
29
24
|
clear(): void;
|
|
30
25
|
forEach(callbackFn: (value: V, key: K, collection: Collection<K, V>) => void, thisArg?: unknown): void;
|
|
31
26
|
get size(): number;
|
|
32
27
|
add(value: K): this;
|
|
33
28
|
delete(value: K): boolean;
|
|
29
|
+
private wrapSetValue;
|
|
34
30
|
has(value: K): boolean;
|
|
35
31
|
entries(): IterableIterator<[K, V]>;
|
|
36
32
|
keys(): IterableIterator<K>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { getAdministration, getSource, isObservable, getObservable,
|
|
1
|
+
export { getAdministration, getSource, isObservable, getObservable, getInternalNode, createObservableWithCustomAdministration, Observable, } from "./internal/lookup";
|
|
2
2
|
export { ArrayAdministration } from "./array";
|
|
3
3
|
export { CollectionAdministration } from "./collection";
|
|
4
4
|
export { DateAdministration } from "./date";
|
|
5
5
|
export { ObjectAdministration } from "./object";
|
|
6
|
-
export { type AtomNode, reaction, type SignalNode, createAtom, batch, createComputed, createSignal, untracked, type ListenerNode, createListener, type ComputedNode, PreactObjectAdministration, getSignal, source, reportChanged, reportObserved, observable, computed, Signal, type ReadonlySignal, effect, signal,
|
|
6
|
+
export { type AtomNode, reaction, type SignalNode, createAtom, batch, createComputed, createSignal, untracked, type ListenerNode, createListener, type ComputedNode, PreactObjectAdministration, getSignal, source, reportChanged, reportObserved, observable, computed, Signal, type ReadonlySignal, effect, signal, } from "./preact";
|
|
@@ -7,11 +7,10 @@ export declare class Administration<T extends object = any> {
|
|
|
7
7
|
readonly atom: ObservedAtomNode;
|
|
8
8
|
protected valuesMap?: SignalMap;
|
|
9
9
|
protected isObserved: boolean;
|
|
10
|
-
private
|
|
10
|
+
private forceObservedAtom?;
|
|
11
11
|
constructor(source: T);
|
|
12
12
|
protected flushChange(): void;
|
|
13
13
|
getNode(): unknown;
|
|
14
14
|
reportChanged(): void;
|
|
15
|
-
|
|
16
|
-
reportObserved(deep?: boolean): void;
|
|
15
|
+
reportObserved(): void;
|
|
17
16
|
}
|
|
@@ -13,6 +13,7 @@ declare class NodeMap<K = unknown, GraphNode extends AtomNode | SignalNode<unkno
|
|
|
13
13
|
getOrCreate(key: K, value?: unknown): GraphNode;
|
|
14
14
|
reportObserved(key: K, value?: unknown): void;
|
|
15
15
|
reportChanged(key: K, value?: unknown): void;
|
|
16
|
+
keys(): Iterable<unknown>;
|
|
16
17
|
}
|
|
17
18
|
export declare class AtomMap<K = unknown> extends NodeMap<K, AtomNode> {
|
|
18
19
|
protected createNode(): AtomNode;
|
|
@@ -7,10 +7,11 @@ export declare function getAdministration<T extends object>(obj: T): T extends S
|
|
|
7
7
|
export declare function getSource<T>(obj: T): T;
|
|
8
8
|
export declare function getAction<T extends Function>(fn: T): T;
|
|
9
9
|
export declare function getObservableClassInstance<T extends object>(value: T): T;
|
|
10
|
+
export declare class Observable {
|
|
11
|
+
constructor();
|
|
12
|
+
}
|
|
10
13
|
export declare function getObservableIfExists<T>(value: T): T | undefined;
|
|
11
14
|
export declare function createObservableWithCustomAdministration<T>(value: T, Adm: new (obj: any) => Administration): T;
|
|
12
15
|
export declare function getObservable<T>(value: T): T;
|
|
13
|
-
export declare function isShallowObservable(obj: unknown): boolean;
|
|
14
|
-
export declare function getShallowObservable<T>(value: T): T;
|
|
15
16
|
export declare function isObservable(obj: unknown): boolean;
|
|
16
17
|
export declare function getInternalNode(obj: object, key?: PropertyKey): any;
|