mutts 1.0.10 → 1.0.12
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 +3 -3
- package/dist/browser.cjs +244 -3199
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.ts +2 -2
- package/dist/browser.dev.cjs +51 -50
- package/dist/browser.dev.cjs.map +1 -1
- package/dist/browser.dev.d.ts +2 -2
- package/dist/browser.dev.esm.js +2 -2
- package/dist/browser.esm.js +15 -13
- package/dist/browser.esm.js.map +1 -1
- package/dist/chunks/{index-CaaQQlPJ.esm.js → index-BUop6B2U.esm.js} +384 -981
- package/dist/chunks/index-BUop6B2U.esm.js.map +1 -0
- package/dist/chunks/index-yK0HVxHv.cjs +2612 -0
- package/dist/chunks/index-yK0HVxHv.cjs.map +1 -0
- package/dist/chunks/{node-nKJBk8iJ.esm.js → node-Bo7WU5S2.esm.js} +2 -2
- package/dist/chunks/{node-nKJBk8iJ.esm.js.map → node-Bo7WU5S2.esm.js.map} +1 -1
- package/dist/chunks/{async-node-3PrbVAbB.cjs → node-Dd0esp5F.cjs} +4 -4
- package/dist/chunks/node-Dd0esp5F.cjs.map +1 -0
- package/dist/chunks/{proxy-Dtg-bJ3T.cjs → proxy-BvM4yewA.cjs} +441 -342
- package/dist/chunks/proxy-BvM4yewA.cjs.map +1 -0
- package/dist/chunks/{proxy-r7lARftl.esm.js → proxy-D2C49sXH.esm.js} +401 -330
- package/dist/chunks/proxy-D2C49sXH.esm.js.map +1 -0
- package/dist/debug.cjs +17 -4
- package/dist/debug.cjs.map +1 -1
- package/dist/debug.d.ts +2 -2
- package/dist/debug.esm.js +17 -3
- package/dist/debug.esm.js.map +1 -1
- package/dist/index.d.ts +84 -209
- package/dist/mutts.umd.js +842 -1362
- package/dist/mutts.umd.js.map +1 -1
- package/dist/mutts.umd.min.js +1 -1
- package/dist/mutts.umd.min.js.map +1 -1
- package/dist/node.cjs +51 -49
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.ts +2 -2
- package/dist/node.dev.cjs +51 -49
- package/dist/node.dev.cjs.map +1 -1
- package/dist/node.dev.d.ts +2 -2
- package/dist/node.dev.esm.js +3 -3
- package/dist/node.esm.js +3 -3
- package/dist/{types-W5vD6m2n.d.ts → types-Bx2PhORg.d.ts} +38 -47
- package/docs/ai/api-reference.md +0 -14
- package/docs/ai/manual.md +2 -22
- package/docs/reactive/advanced.md +13 -14
- package/docs/reactive/attend.md +1 -2
- package/docs/reactive/collections.md +2 -149
- package/docs/reactive/core.md +218 -96
- package/docs/reactive/debugging.md +2 -2
- package/docs/reactive/resource.md +1 -1
- package/docs/reactive.md +1 -3
- package/docs/zone.md +1 -1
- package/package.json +18 -9
- package/dist/chunks/async-browser-BU_IfxYD.cjs +0 -216
- package/dist/chunks/async-browser-BU_IfxYD.cjs.map +0 -1
- package/dist/chunks/async-core-CRLKP3l-.cjs +0 -29
- package/dist/chunks/async-core-CRLKP3l-.cjs.map +0 -1
- package/dist/chunks/async-node-3PrbVAbB.cjs.map +0 -1
- package/dist/chunks/index-CaaQQlPJ.esm.js.map +0 -1
- package/dist/chunks/proxy-Dtg-bJ3T.cjs.map +0 -1
- package/dist/chunks/proxy-r7lARftl.esm.js.map +0 -1
- package/docs/reactive/scan.md +0 -324
package/docs/reactive/core.md
CHANGED
|
@@ -4,23 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
- [Introduction](#introduction)
|
|
6
6
|
- [Getting Started](#getting-started)
|
|
7
|
-
|
|
7
|
+
- [5-Minute Quick Start](#5-minute-quick-start)
|
|
8
8
|
- [Core API](#core-api)
|
|
9
9
|
- [Effect System](#effect-system)
|
|
10
|
-
|
|
10
|
+
- [Error Handling](./error-handling.md)
|
|
11
11
|
- [Atomic Operations](./advanced.md#atomic-operations)
|
|
12
12
|
- [Advanced Effects](./advanced.md#advanced-effects)
|
|
13
13
|
- [Evolution Tracking](./advanced.md#evolution-tracking)
|
|
14
14
|
- [Prototype Chains and Pure Objects](./advanced.md#prototype-chains-and-pure-objects)
|
|
15
15
|
- [Recursive Touching](./advanced.md#recursive-touching)
|
|
16
16
|
- [Collections](./collections.md)
|
|
17
|
-
- [Register](./collections.md#register)
|
|
18
17
|
- [Class Reactivity](#class-reactivity)
|
|
19
18
|
- [Non-Reactive System](#non-reactive-system)
|
|
20
19
|
- [Morphing](./advanced.md#morph)
|
|
21
20
|
- [Memoization](./advanced.md#memoization)
|
|
22
21
|
- [Debugging and Development](./advanced.md#debugging-and-development)
|
|
23
|
-
|
|
22
|
+
- [Cycle Detection](./advanced.md#cycle-detection)
|
|
24
23
|
|
|
25
24
|
## Introduction
|
|
26
25
|
|
|
@@ -40,10 +39,13 @@ Reactivity is a programming paradigm where the system automatically tracks depen
|
|
|
40
39
|
### Basic Example
|
|
41
40
|
|
|
42
41
|
```typescript
|
|
43
|
-
import { reactive, effect } from 'mutts
|
|
42
|
+
import { reactive, effect } from 'mutts'
|
|
44
43
|
|
|
45
44
|
// Create a reactive object
|
|
46
|
-
const user = reactive({
|
|
45
|
+
const user = reactive({
|
|
46
|
+
name: "John",
|
|
47
|
+
age: 30
|
|
48
|
+
})
|
|
47
49
|
|
|
48
50
|
// Create an effect that depends on user properties
|
|
49
51
|
effect(() => {
|
|
@@ -51,8 +53,8 @@ effect(() => {
|
|
|
51
53
|
})
|
|
52
54
|
|
|
53
55
|
// When properties change, the effect automatically re-runs
|
|
54
|
-
user.name = "Jane"
|
|
55
|
-
user.age = 25
|
|
56
|
+
user.name = "Jane" // Triggers effect
|
|
57
|
+
user.age = 25 // Triggers effect
|
|
56
58
|
```
|
|
57
59
|
|
|
58
60
|
## Getting Started
|
|
@@ -62,38 +64,45 @@ user.age = 25 // Triggers effect
|
|
|
62
64
|
Learn the essentials in 5 minutes:
|
|
63
65
|
|
|
64
66
|
**1. Make state reactive:**
|
|
67
|
+
|
|
65
68
|
```typescript
|
|
66
|
-
import { reactive, effect } from 'mutts
|
|
69
|
+
import { reactive, effect } from 'mutts'
|
|
67
70
|
|
|
68
|
-
const state = reactive({
|
|
71
|
+
const state = reactive({
|
|
72
|
+
count: 0,
|
|
73
|
+
name: "John"
|
|
74
|
+
})
|
|
69
75
|
```
|
|
70
76
|
|
|
71
77
|
**2. React to changes:**
|
|
78
|
+
|
|
72
79
|
```typescript
|
|
73
80
|
// Effects automatically re-run when dependencies change
|
|
74
81
|
effect(() => {
|
|
75
|
-
|
|
82
|
+
console.log(`Hello ${state.name}, count is ${state.count}`)
|
|
76
83
|
})
|
|
77
84
|
|
|
78
|
-
state.count++
|
|
79
|
-
state.name = "Jane"
|
|
85
|
+
state.count++ // Triggers effect
|
|
86
|
+
state.name = "Jane" // Triggers effect
|
|
80
87
|
```
|
|
81
88
|
|
|
82
89
|
**3. Work with arrays:**
|
|
90
|
+
|
|
83
91
|
```typescript
|
|
84
92
|
const items = reactive([1, 2, 3])
|
|
85
93
|
|
|
86
94
|
effect(() => {
|
|
87
|
-
|
|
95
|
+
console.log(`Array length: ${items.length}`)
|
|
88
96
|
})
|
|
89
97
|
|
|
90
|
-
items.push(4)
|
|
98
|
+
items.push(4) // Triggers effect
|
|
91
99
|
```
|
|
92
100
|
|
|
93
101
|
**4. Use memoization:**
|
|
102
|
+
|
|
94
103
|
```typescript
|
|
95
104
|
const memoized = memoize((user: User) => {
|
|
96
|
-
|
|
105
|
+
return expensiveComputation(user)
|
|
97
106
|
})
|
|
98
107
|
|
|
99
108
|
// Only recomputes when user changes
|
|
@@ -101,12 +110,12 @@ const result = memoized(user)
|
|
|
101
110
|
```
|
|
102
111
|
|
|
103
112
|
**5. Map over arrays:**
|
|
113
|
+
|
|
104
114
|
```typescript
|
|
105
115
|
const source = reactive([1, 2, 3])
|
|
106
|
-
const doubled = morph(source,
|
|
107
|
-
// [2, 4, 6]
|
|
116
|
+
const doubled = morph(source, value => value * 2) // [2, 4, 6]
|
|
108
117
|
|
|
109
|
-
source.push(4)
|
|
118
|
+
source.push(4) // doubled automatically becomes [2, 4, 6, 8]
|
|
110
119
|
```
|
|
111
120
|
|
|
112
121
|
**Ready to go!** Continue reading for advanced features.
|
|
@@ -122,7 +131,7 @@ npm install mutts
|
|
|
122
131
|
### Basic Usage
|
|
123
132
|
|
|
124
133
|
```typescript
|
|
125
|
-
import { reactive, effect } from 'mutts
|
|
134
|
+
import { reactive, effect } from 'mutts'
|
|
126
135
|
|
|
127
136
|
// Make an object reactive
|
|
128
137
|
const state = reactive({
|
|
@@ -140,17 +149,19 @@ effect(() => {
|
|
|
140
149
|
})
|
|
141
150
|
|
|
142
151
|
// Changes trigger effects automatically
|
|
143
|
-
state.count++
|
|
152
|
+
state.count++ // Triggers first effect
|
|
144
153
|
state.message = "Hi" // Triggers second effect
|
|
145
154
|
```
|
|
146
155
|
|
|
147
156
|
### Hello World Example
|
|
148
157
|
|
|
149
158
|
```typescript
|
|
150
|
-
import { reactive, effect } from 'mutts
|
|
159
|
+
import { reactive, effect } from 'mutts'
|
|
151
160
|
|
|
152
161
|
// Simple counter
|
|
153
|
-
const counter = reactive({
|
|
162
|
+
const counter = reactive({
|
|
163
|
+
value: 0
|
|
164
|
+
})
|
|
154
165
|
|
|
155
166
|
effect(() => {
|
|
156
167
|
document.body.innerHTML = `Count: ${counter.value}`
|
|
@@ -178,11 +189,14 @@ function reactive<T extends Record<PropertyKey, any>>(target: T): T
|
|
|
178
189
|
**Returns:** A reactive proxy of the original object
|
|
179
190
|
|
|
180
191
|
**Example:**
|
|
192
|
+
|
|
181
193
|
```typescript
|
|
182
|
-
const obj = {
|
|
183
|
-
|
|
194
|
+
const obj = {
|
|
195
|
+
count: 0
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const reactiveObj = reactive(obj) // reactiveObj is now reactive
|
|
184
199
|
|
|
185
|
-
// reactiveObj is now reactive
|
|
186
200
|
effect(() => {
|
|
187
201
|
console.log(reactiveObj.count) // Tracks dependency
|
|
188
202
|
})
|
|
@@ -192,9 +206,7 @@ reactiveObj.count = 5 // Triggers effect
|
|
|
192
206
|
|
|
193
207
|
**Note:** The same object will always return the same proxy instance.
|
|
194
208
|
|
|
195
|
-
**Pure Objects and Prototypes:**
|
|
196
|
-
|
|
197
|
-
`reactive()` works with any object type, including:
|
|
209
|
+
**Pure Objects and Prototypes:** `reactive()` works with any object type, including:
|
|
198
210
|
- Normal objects: `reactive({ x: 1 })`
|
|
199
211
|
- Pure objects: `reactive(Object.create(null))`
|
|
200
212
|
- Objects with prototypes: `reactive(Object.create(parent))`
|
|
@@ -208,9 +220,9 @@ Creates a reactive effect that automatically re-runs when dependencies change.
|
|
|
208
220
|
|
|
209
221
|
```typescript
|
|
210
222
|
interface DependencyAccess {
|
|
211
|
-
tracked: DependencyFunction
|
|
212
|
-
ascend: DependencyFunction
|
|
213
|
-
reaction: boolean
|
|
223
|
+
tracked: DependencyFunction // Track dependencies in the current effect
|
|
224
|
+
ascend: DependencyFunction // Track dependencies in the parent effect
|
|
225
|
+
reaction: boolean // true after the first run
|
|
214
226
|
}
|
|
215
227
|
|
|
216
228
|
function effect(
|
|
@@ -231,16 +243,21 @@ function effect(
|
|
|
231
243
|
**Returns:** A cleanup function to stop the effect
|
|
232
244
|
|
|
233
245
|
**Example:**
|
|
246
|
+
|
|
234
247
|
```typescript
|
|
235
|
-
const state = reactive({
|
|
248
|
+
const state = reactive({
|
|
249
|
+
count: 0,
|
|
250
|
+
mood: 'happy'
|
|
251
|
+
})
|
|
236
252
|
|
|
237
253
|
const cleanup = effect(() => {
|
|
238
254
|
console.log(`Count is: ${state.count}`)
|
|
255
|
+
|
|
239
256
|
// Optional cleanup called before next run
|
|
240
257
|
return () => console.log('Cleaning up...')
|
|
241
258
|
})
|
|
242
259
|
|
|
243
|
-
state.count++
|
|
260
|
+
state.count++ // Does trigger: 1- the cleaning, 2- the effect
|
|
244
261
|
state.mood = 'surprised' // Does not trigger the effect
|
|
245
262
|
|
|
246
263
|
// Later...
|
|
@@ -260,8 +277,13 @@ effect(({ reaction }) => {
|
|
|
260
277
|
```
|
|
261
278
|
|
|
262
279
|
**Using effect with arguments (useful in loops):**
|
|
280
|
+
|
|
263
281
|
```typescript
|
|
264
|
-
const items = reactive([
|
|
282
|
+
const items = reactive([
|
|
283
|
+
{ id: 1 },
|
|
284
|
+
{ id: 2 },
|
|
285
|
+
{ id: 3 }
|
|
286
|
+
])
|
|
265
287
|
|
|
266
288
|
// Create effects in a loop, passing loop variables
|
|
267
289
|
for (let i = 0; i < items.length; i++) {
|
|
@@ -280,8 +302,12 @@ function unwrap<T>(proxy: T): T
|
|
|
280
302
|
```
|
|
281
303
|
|
|
282
304
|
**Example:**
|
|
305
|
+
|
|
283
306
|
```typescript
|
|
284
|
-
const original = {
|
|
307
|
+
const original = {
|
|
308
|
+
count: 0
|
|
309
|
+
}
|
|
310
|
+
|
|
285
311
|
const reactive = reactive(original)
|
|
286
312
|
const unwrapped = unwrap(reactive)
|
|
287
313
|
|
|
@@ -312,7 +338,10 @@ function isNonReactive(obj: any): boolean
|
|
|
312
338
|
Effects are the core of the reactive system. They automatically track dependencies and re-run when those dependencies change.
|
|
313
339
|
|
|
314
340
|
```typescript
|
|
315
|
-
const state = reactive({
|
|
341
|
+
const state = reactive({
|
|
342
|
+
count: 0,
|
|
343
|
+
name: "John"
|
|
344
|
+
})
|
|
316
345
|
|
|
317
346
|
effect(() => {
|
|
318
347
|
// This effect depends on state.count
|
|
@@ -320,7 +349,7 @@ effect(() => {
|
|
|
320
349
|
})
|
|
321
350
|
|
|
322
351
|
// Only changing count triggers the effect
|
|
323
|
-
state.count = 5
|
|
352
|
+
state.count = 5 // Triggers effect
|
|
324
353
|
state.name = "Jane" // Does NOT trigger effect
|
|
325
354
|
```
|
|
326
355
|
|
|
@@ -329,7 +358,9 @@ state.name = "Jane" // Does NOT trigger effect
|
|
|
329
358
|
Effects return cleanup functions that you can call to stop tracking dependencies.
|
|
330
359
|
|
|
331
360
|
```typescript
|
|
332
|
-
const state = reactive({
|
|
361
|
+
const state = reactive({
|
|
362
|
+
count: 0
|
|
363
|
+
})
|
|
333
364
|
|
|
334
365
|
const stopEffect = effect(() => {
|
|
335
366
|
console.log(`Count: ${state.count}`)
|
|
@@ -349,13 +380,17 @@ The reactive system provides **automatic cleanup** for effects, making memory ma
|
|
|
349
380
|
|
|
350
381
|
1. **Parent-Child Cleanup**: When an effect is created inside another effect, it becomes a "child" of the parent effect. When the parent effect is cleaned up, it also cleans up its child effects.
|
|
351
382
|
|
|
352
|
-
2. **Garbage Collection Cleanup**: For top-level effects (not created inside other effects), the system uses JavaScript's garbage collection to automatically clean them up when their cleanup function no longer referenced.
|
|
383
|
+
2. **Garbage Collection Cleanup**: For top-level effects (not created inside other effects), the system uses JavaScript's garbage collection to automatically clean them up when their cleanup function is no longer referenced.
|
|
353
384
|
|
|
354
385
|
#### Examples
|
|
355
386
|
|
|
356
387
|
**Parent-Child Cleanup:**
|
|
388
|
+
|
|
357
389
|
```typescript
|
|
358
|
-
const state = reactive({
|
|
390
|
+
const state = reactive({
|
|
391
|
+
a: 1,
|
|
392
|
+
b: 2
|
|
393
|
+
})
|
|
359
394
|
|
|
360
395
|
const stopParent = effect(() => {
|
|
361
396
|
state.a
|
|
@@ -374,8 +409,11 @@ stopParent() // Logs (order may vary): "Child cleanup", then "Parent cleanup"
|
|
|
374
409
|
```
|
|
375
410
|
|
|
376
411
|
**Garbage Collection Cleanup:**
|
|
412
|
+
|
|
377
413
|
```typescript
|
|
378
|
-
const state = reactive({
|
|
414
|
+
const state = reactive({
|
|
415
|
+
value: 1
|
|
416
|
+
})
|
|
379
417
|
|
|
380
418
|
// Top-level effect - automatically cleaned up via garbage collection
|
|
381
419
|
effect(() => {
|
|
@@ -391,7 +429,10 @@ effect(() => {
|
|
|
391
429
|
While cleanup can be automatic via GC, you should **store and remember** cleanup functions both to prevent the effect from being garbage-collected (keeping it alive) and to perform immediate cleanup when needed. If you don't hold a reference to the cleanup (or to the effect), the effect can be collected and its cleanup called automatically by GC; storing a reference keeps it active under your control:
|
|
392
430
|
|
|
393
431
|
```typescript
|
|
394
|
-
const state = reactive({
|
|
432
|
+
const state = reactive({
|
|
433
|
+
value: 1
|
|
434
|
+
})
|
|
435
|
+
|
|
395
436
|
const activeEffects: (() => void)[] = []
|
|
396
437
|
|
|
397
438
|
// Store cleanup functions for effects with side effects
|
|
@@ -417,19 +458,79 @@ activeEffects.forEach(stop => stop())
|
|
|
417
458
|
```
|
|
418
459
|
|
|
419
460
|
**Key Points:**
|
|
420
|
-
|
|
421
461
|
- **You do not have to call cleanup** - GC may clean up effects when no references remain
|
|
422
462
|
- **You may want to call cleanup** - especially for effects with side effects
|
|
423
463
|
- **Store cleanup references to keep effects alive** - holding a reference prevents GC cleanup and gives you explicit stop control
|
|
424
464
|
- **Parent cleanup cleans child effects** - stopping a parent also stops its child effects
|
|
425
465
|
- **Child effects are referenced by parent effects** - and therefore are not subject to GC cleanups
|
|
426
466
|
|
|
467
|
+
### Object Lifecycle with `link` / `unlink`
|
|
468
|
+
|
|
469
|
+
While effects handle their own cleanup automatically, **derived reactive objects** (morph results, lifted arrays, processed children, etc.) often need explicit lifecycle management. `link` and `unlink` solve this by forming a **cleanup tree** — an ownership graph independent of the effect hierarchy.
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
import { link, unlink } from 'mutts'
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
#### Attaching cleanup dependencies
|
|
476
|
+
|
|
477
|
+
`link(owner, ...deps)` attaches dependencies to an owner object. Dependencies can be:
|
|
478
|
+
- **Functions** — called with an optional `CleanupReason` on disposal
|
|
479
|
+
- **Objects** — recursively `unlink`ed on disposal
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
const parent = reactive({
|
|
483
|
+
items: []
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
// Attach a cleanup callback
|
|
487
|
+
link(parent, () => console.log('parent disposed'))
|
|
488
|
+
|
|
489
|
+
// Attach child objects — they will be recursively unlinked
|
|
490
|
+
const childA = morph(parent.items, ({ value }) => value * 2)
|
|
491
|
+
const childB = morph(parent.items, ({ value }) => value + 1)
|
|
492
|
+
|
|
493
|
+
link(parent, childA, childB)
|
|
494
|
+
|
|
495
|
+
// Mixed: objects and functions together
|
|
496
|
+
link(parent, childA, () => timer.clear())
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
#### Disposing an object
|
|
500
|
+
|
|
501
|
+
`unlink(obj)` disposes all dependencies attached to `obj`:
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
unlink(parent)
|
|
505
|
+
|
|
506
|
+
// 1. childA is recursively unlinked (its own deps disposed)
|
|
507
|
+
// 2. childB is recursively unlinked
|
|
508
|
+
// 3. the callback runs: "parent disposed"
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
Calling `unlink` twice is safe — the second call is a no-op. Not calling `unlink` is also fine — the object will be cleaned up when garbage is collected.
|
|
512
|
+
|
|
513
|
+
#### When to use `link`/`unlink` vs effects
|
|
514
|
+
|
|
515
|
+
| Scenario | Use |
|
|
516
|
+
|---|---|
|
|
517
|
+
| Reacting to state changes | `effect` |
|
|
518
|
+
| Tying a derived object's lifetime to its owner | `link` / `unlink` |
|
|
519
|
+
| Cleaning up timers, listeners, DOM nodes | `effect` return or `link` callback |
|
|
520
|
+
| Building a cleanup tree across multiple reactive objects | `link` / `unlink` |
|
|
521
|
+
|
|
522
|
+
**Rule of thumb**: if the thing you're cleaning up is a *reactive object* (not a function), prefer `link`. If it's a *side effect* (timer, listener), prefer an `effect` return or a `link` callback.
|
|
523
|
+
|
|
427
524
|
### Effect Dependencies
|
|
428
525
|
|
|
429
526
|
Effects automatically track which reactive properties they access.
|
|
430
527
|
|
|
431
528
|
```typescript
|
|
432
|
-
const state = reactive({
|
|
529
|
+
const state = reactive({
|
|
530
|
+
a: 1,
|
|
531
|
+
b: 2,
|
|
532
|
+
c: 3
|
|
533
|
+
})
|
|
433
534
|
|
|
434
535
|
effect(() => {
|
|
435
536
|
// Only tracks state.a and state.b
|
|
@@ -444,13 +545,11 @@ state.c = 15 // Does NOT trigger effect
|
|
|
444
545
|
|
|
445
546
|
### Async Effects and the `access` Parameter
|
|
446
547
|
|
|
447
|
-
The `effect` function provides a special `access` parameter with `tracked` and `ascend` functions that restore the active effect context for dependency tracking in asynchronous operations.
|
|
448
|
-
|
|
449
|
-
In modern `mutts`, this is powered by the **Zone system**. When zones are registered in `asyncZone`, the active effect context is automatically preserved across `await` points and timers, making manual use of `tracked` optional for these cases.
|
|
548
|
+
The `effect` function provides a special `access` parameter with `tracked` and `ascend` functions that restore the active effect context for dependency tracking in asynchronous operations. In modern `mutts`, this is powered by the **Zone system**. When zones are registered in `asyncZone`, the active effect context is automatically preserved across `await` points and timers, making manual use of `tracked` optional for these cases.
|
|
450
549
|
|
|
451
550
|
#### The Problem with Async Effects
|
|
452
551
|
|
|
453
|
-
Traditionally, in JavaScript, async functions lose their context when they yield control.
|
|
552
|
+
Traditionally, in JavaScript, async functions lose their context when they yield control.
|
|
454
553
|
|
|
455
554
|
#### Understanding the active effect context
|
|
456
555
|
|
|
@@ -467,36 +566,28 @@ effect(() => {
|
|
|
467
566
|
// Async effect WITHOUT zone registration - context is lost after await
|
|
468
567
|
effect(async () => {
|
|
469
568
|
const value = state.count // ✅ Tracked
|
|
470
|
-
|
|
471
|
-
await someAsyncOperation()
|
|
472
|
-
|
|
473
|
-
// context is lost!
|
|
569
|
+
await someAsyncOperation() // context is lost!
|
|
474
570
|
const another = state.name // ❌ NOT tracked
|
|
475
571
|
})
|
|
476
572
|
|
|
477
573
|
// Async effect WITH zone registration - context is preserved
|
|
478
574
|
effect(async () => {
|
|
479
575
|
const value = state.count // ✅ Tracked
|
|
480
|
-
|
|
481
|
-
await someAsyncOperation()
|
|
482
|
-
|
|
483
|
-
// context is automatically restored!
|
|
576
|
+
await someAsyncOperation() // context is automatically restored!
|
|
484
577
|
const another = state.name // ✅ Tracked
|
|
485
578
|
})
|
|
486
579
|
|
|
487
580
|
// Using access.tracked() for manual restoration
|
|
488
581
|
effect(async ({ tracked }) => {
|
|
489
|
-
await someAsyncOperation()
|
|
490
|
-
|
|
491
|
-
// Useful for non-patched APIs or explicit scoping
|
|
582
|
+
await someAsyncOperation() // Useful for non-patched APIs or explicit scoping
|
|
492
583
|
const another = tracked(() => state.name) // ✅ Tracked
|
|
493
584
|
})
|
|
494
585
|
```
|
|
495
586
|
|
|
496
587
|
#### Key Benefits of the Zone System
|
|
497
588
|
|
|
498
|
-
1.
|
|
499
|
-
2.
|
|
589
|
+
1. **Automatic Restoration**: With zones registered in `asyncZone`, most native async APIs (Promises, timers) automatically preserve the reactive context.
|
|
590
|
+
2. **Manual Control**: `access.tracked()` allows you to manually "passport" the context into third-party libraries or unmanaged callbacks.
|
|
500
591
|
|
|
501
592
|
### Using `ascend` for Parent Effect Tracking
|
|
502
593
|
|
|
@@ -532,18 +623,21 @@ effect(({ ascend }) => {
|
|
|
532
623
|
Effects can be created inside other effects and will have separate effect scopes:
|
|
533
624
|
|
|
534
625
|
```typescript
|
|
535
|
-
import { effect, reactive } from 'mutts
|
|
626
|
+
import { effect, reactive } from 'mutts'
|
|
536
627
|
|
|
537
|
-
const state = reactive({
|
|
628
|
+
const state = reactive({
|
|
629
|
+
a: 0,
|
|
630
|
+
b: 0
|
|
631
|
+
})
|
|
538
632
|
|
|
539
633
|
const stopOuter = effect(() => {
|
|
540
634
|
state.a
|
|
541
|
-
|
|
635
|
+
|
|
542
636
|
// Create an inner effect with its own scope
|
|
543
637
|
const stopInner = effect(() => {
|
|
544
638
|
state.b
|
|
545
639
|
})
|
|
546
|
-
|
|
640
|
+
|
|
547
641
|
// Return cleanup function for the inner effect
|
|
548
642
|
return stopInner
|
|
549
643
|
})
|
|
@@ -554,21 +648,25 @@ const stopOuter = effect(() => {
|
|
|
554
648
|
The `untracked()` function allows you to run code without tracking dependencies, which can be useful for creating effects or performing operations that shouldn't be part of the current effect's dependency graph.
|
|
555
649
|
|
|
556
650
|
```typescript
|
|
557
|
-
import { effect, untracked, reactive } from 'mutts
|
|
651
|
+
import { effect, untracked, reactive } from 'mutts'
|
|
558
652
|
|
|
559
|
-
const state = reactive({
|
|
653
|
+
const state = reactive({
|
|
654
|
+
a: 0,
|
|
655
|
+
b: 0
|
|
656
|
+
})
|
|
560
657
|
|
|
561
658
|
effect(() => {
|
|
562
659
|
state.a
|
|
563
|
-
|
|
660
|
+
|
|
564
661
|
// Create an inner effect without tracking the creation under the outer effect
|
|
565
662
|
let stopInner: (() => void) | undefined
|
|
663
|
+
|
|
566
664
|
untracked(() => {
|
|
567
665
|
stopInner = effect(() => {
|
|
568
666
|
state.b
|
|
569
667
|
})
|
|
570
668
|
})
|
|
571
|
-
|
|
669
|
+
|
|
572
670
|
// Optionally stop it immediately to avoid accumulating watchers
|
|
573
671
|
stopInner && stopInner()
|
|
574
672
|
})
|
|
@@ -584,7 +682,7 @@ effect(() => {
|
|
|
584
682
|
Configure the reactive system behavior:
|
|
585
683
|
|
|
586
684
|
```typescript
|
|
587
|
-
import { options as reactiveOptions } from 'mutts
|
|
685
|
+
import { options as reactiveOptions } from 'mutts'
|
|
588
686
|
|
|
589
687
|
// Set maximum effect chain depth
|
|
590
688
|
reactiveOptions.maxEffectChain = 50
|
|
@@ -610,9 +708,14 @@ The `effect` function provides convenient shortcut modifiers for common options.
|
|
|
610
708
|
Creates an opaque effect that tracks object references rather than deep content. This is useful when you want effects to re-run only when the object identity changes, not when its properties change.
|
|
611
709
|
|
|
612
710
|
```typescript
|
|
613
|
-
import { effect, reactive } from 'mutts
|
|
711
|
+
import { effect, reactive } from 'mutts'
|
|
614
712
|
|
|
615
|
-
const item = reactive({
|
|
713
|
+
const item = reactive({
|
|
714
|
+
id: 1,
|
|
715
|
+
data: {
|
|
716
|
+
value: 10
|
|
717
|
+
}
|
|
718
|
+
})
|
|
616
719
|
|
|
617
720
|
// Regular effect - triggers on any property change
|
|
618
721
|
effect(() => {
|
|
@@ -624,8 +727,9 @@ effect.opaque(() => {
|
|
|
624
727
|
console.log('Data object:', item.data) // Only triggers when item.data is replaced
|
|
625
728
|
})
|
|
626
729
|
|
|
627
|
-
item.data.value = 20
|
|
628
|
-
|
|
730
|
+
item.data.value = 20 // Triggers regular effect, NOT opaque effect
|
|
731
|
+
|
|
732
|
+
item.data = { value: 30 } // Triggers BOTH effects
|
|
629
733
|
```
|
|
630
734
|
|
|
631
735
|
**Use cases for opaque effects:**
|
|
@@ -638,9 +742,11 @@ item.data = { value: 30 } // Triggers BOTH effects
|
|
|
638
742
|
Creates a named effect for easier debugging and profiling. The name appears in DevTools and debug logs.
|
|
639
743
|
|
|
640
744
|
```typescript
|
|
641
|
-
import { effect, reactive } from 'mutts
|
|
745
|
+
import { effect, reactive } from 'mutts'
|
|
642
746
|
|
|
643
|
-
const state = reactive({
|
|
747
|
+
const state = reactive({
|
|
748
|
+
count: 0
|
|
749
|
+
})
|
|
644
750
|
|
|
645
751
|
// Create a named effect
|
|
646
752
|
effect.named('counter-effect')(() => {
|
|
@@ -689,13 +795,14 @@ createDataEffect(() => {
|
|
|
689
795
|
})
|
|
690
796
|
```
|
|
691
797
|
|
|
798
|
+
## Class Reactivity
|
|
692
799
|
|
|
693
800
|
### `@reactive` Decorator
|
|
694
801
|
|
|
695
802
|
The `@reactive` decorator makes class instances automatically reactive. This is the recommended approach for adding reactivity to classes.
|
|
696
803
|
|
|
697
804
|
```typescript
|
|
698
|
-
import { reactive } from 'mutts
|
|
805
|
+
import { reactive } from 'mutts'
|
|
699
806
|
|
|
700
807
|
@reactive
|
|
701
808
|
class User {
|
|
@@ -727,7 +834,7 @@ user.name = "Jane" // Triggers effect
|
|
|
727
834
|
You can also use the functional syntax for making classes reactive:
|
|
728
835
|
|
|
729
836
|
```typescript
|
|
730
|
-
import { reactive } from 'mutts
|
|
837
|
+
import { reactive } from 'mutts'
|
|
731
838
|
|
|
732
839
|
class User {
|
|
733
840
|
name: string
|
|
@@ -759,11 +866,14 @@ user.name = "Jane" // Triggers effect
|
|
|
759
866
|
For complex inheritance trees, especially when you need to solve constructor reactivity issues, extend `ReactiveBase`:
|
|
760
867
|
|
|
761
868
|
```typescript
|
|
762
|
-
import { ReactiveBase, reactive } from 'mutts
|
|
869
|
+
import { ReactiveBase, reactive } from 'mutts'
|
|
763
870
|
|
|
764
871
|
class GameObject extends ReactiveBase {
|
|
765
872
|
id = 'game-object'
|
|
766
|
-
position = {
|
|
873
|
+
position = {
|
|
874
|
+
x: 0,
|
|
875
|
+
y: 0
|
|
876
|
+
}
|
|
767
877
|
}
|
|
768
878
|
|
|
769
879
|
class Entity extends GameObject {
|
|
@@ -783,11 +893,10 @@ effect(() => {
|
|
|
783
893
|
})
|
|
784
894
|
|
|
785
895
|
player.position.x = 10 // Triggers effect
|
|
786
|
-
player.health = 80
|
|
896
|
+
player.health = 80 // Triggers effect
|
|
787
897
|
```
|
|
788
898
|
|
|
789
899
|
**Advantages of `ReactiveBase`:**
|
|
790
|
-
|
|
791
900
|
1. **Constructor Reactivity**: Solves the issue where `this` in the constructor is not yet reactive
|
|
792
901
|
2. **Inheritance Safety**: Prevents reactivity from being added to prototype chains in complex inheritance trees
|
|
793
902
|
3. **No Side Effects**: The base class itself has no effect - it only enables proper reactivity when combined with `@reactive`
|
|
@@ -799,7 +908,7 @@ player.health = 80 // Triggers effect
|
|
|
799
908
|
- You want the cleanest, most modern syntax
|
|
800
909
|
- You don't need to modify or use `this` in the constructor
|
|
801
910
|
|
|
802
|
-
**Use `ReactiveBase
|
|
911
|
+
**Use `ReactiveBase`+`@reactive` when:**
|
|
803
912
|
- You have complex inheritance trees (like game objects, UI components)
|
|
804
913
|
- You need to modify or use `this` in the constructor
|
|
805
914
|
- You want to prevent reactivity from being added to prototype chains
|
|
@@ -854,7 +963,7 @@ effect(() => {
|
|
|
854
963
|
console.log('Cart items:', cart.items)
|
|
855
964
|
})
|
|
856
965
|
|
|
857
|
-
cart.addItem('Apple')
|
|
966
|
+
cart.addItem('Apple') // Triggers effect
|
|
858
967
|
cart.removeItem('Apple') // Triggers effect
|
|
859
968
|
```
|
|
860
969
|
|
|
@@ -907,15 +1016,23 @@ function unreactive(target: Constructor<T>): Constructor<T>
|
|
|
907
1016
|
|
|
908
1017
|
```typescript
|
|
909
1018
|
// Mark individual object as non-reactive
|
|
910
|
-
const obj = {
|
|
1019
|
+
const obj = {
|
|
1020
|
+
count: 0
|
|
1021
|
+
}
|
|
1022
|
+
|
|
911
1023
|
unreactive(obj)
|
|
1024
|
+
|
|
912
1025
|
const reactiveObj = reactive(obj) // Returns obj unchanged
|
|
913
1026
|
|
|
914
1027
|
// Mark entire class as non-reactive
|
|
915
1028
|
class Utility {
|
|
916
|
-
static helper() {
|
|
1029
|
+
static helper() {
|
|
1030
|
+
return 'help'
|
|
1031
|
+
}
|
|
917
1032
|
}
|
|
1033
|
+
|
|
918
1034
|
unreactive(Utility)
|
|
1035
|
+
|
|
919
1036
|
const instance = new Utility()
|
|
920
1037
|
const reactiveInstance = reactive(instance) // Returns instance unchanged
|
|
921
1038
|
```
|
|
@@ -929,7 +1046,6 @@ Mark class properties as non-reactive using class-level syntax.
|
|
|
929
1046
|
@unreactive('id')
|
|
930
1047
|
class User {
|
|
931
1048
|
id: string = 'user-123'
|
|
932
|
-
|
|
933
1049
|
name: string = 'John'
|
|
934
1050
|
age: number = 30
|
|
935
1051
|
}
|
|
@@ -938,7 +1054,7 @@ const user = new User()
|
|
|
938
1054
|
|
|
939
1055
|
effect(() => {
|
|
940
1056
|
console.log(user.name, user.age) // Tracks these
|
|
941
|
-
console.log(user.id)
|
|
1057
|
+
console.log(user.id) // Does NOT track this
|
|
942
1058
|
})
|
|
943
1059
|
|
|
944
1060
|
user.name = 'Jane' // Triggers effect
|
|
@@ -1040,23 +1156,29 @@ Non-reactive objects can improve performance:
|
|
|
1040
1156
|
```typescript
|
|
1041
1157
|
// Good: Mark large, rarely-changing objects as non-reactive
|
|
1042
1158
|
const config = unreactive({
|
|
1043
|
-
apiEndpoints: {
|
|
1044
|
-
|
|
1159
|
+
apiEndpoints: {
|
|
1160
|
+
/* large config object */
|
|
1161
|
+
},
|
|
1162
|
+
featureFlags: {
|
|
1163
|
+
/* many flags */
|
|
1164
|
+
}
|
|
1045
1165
|
})
|
|
1046
1166
|
|
|
1047
1167
|
// Good: Mark utility classes as non-reactive
|
|
1048
1168
|
class MathUtils {
|
|
1049
1169
|
static PI = 3.14159
|
|
1050
|
-
|
|
1170
|
+
|
|
1171
|
+
static square(x: number) {
|
|
1172
|
+
return x * x
|
|
1173
|
+
}
|
|
1051
1174
|
}
|
|
1175
|
+
|
|
1052
1176
|
unreactive(MathUtils)
|
|
1053
1177
|
|
|
1054
1178
|
// Good: Mark properties that don't need reactivity
|
|
1055
1179
|
class User {
|
|
1056
|
-
@unreactive
|
|
1057
|
-
metadata: any = {} // Large metadata object
|
|
1180
|
+
@unreactive metadata: any = {} // Large metadata object
|
|
1058
1181
|
|
|
1059
1182
|
name: string = 'John' // This should be reactive
|
|
1060
1183
|
}
|
|
1061
|
-
```
|
|
1062
|
-
|
|
1184
|
+
```
|