mutts 1.0.1 → 1.0.3

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.
Files changed (111) hide show
  1. package/README.md +36 -6
  2. package/dist/chunks/_tslib-BgjropY9.js +81 -0
  3. package/dist/chunks/_tslib-BgjropY9.js.map +1 -0
  4. package/dist/chunks/_tslib-Mzh1rNsX.esm.js +75 -0
  5. package/dist/chunks/_tslib-Mzh1rNsX.esm.js.map +1 -0
  6. package/dist/chunks/{decorator-8qjFb7dw.js → decorator-DLvrD0UF.js} +103 -14
  7. package/dist/chunks/decorator-DLvrD0UF.js.map +1 -0
  8. package/dist/chunks/{decorator-AbRkXM5O.esm.js → decorator-DqiszP7i.esm.js} +100 -15
  9. package/dist/chunks/decorator-DqiszP7i.esm.js.map +1 -0
  10. package/dist/chunks/index-DzUDtFc7.esm.js +4841 -0
  11. package/dist/chunks/index-DzUDtFc7.esm.js.map +1 -0
  12. package/dist/chunks/index-HNVqPzjz.js +4891 -0
  13. package/dist/chunks/index-HNVqPzjz.js.map +1 -0
  14. package/dist/decorator.d.ts +57 -0
  15. package/dist/decorator.esm.js +1 -1
  16. package/dist/decorator.js +1 -1
  17. package/dist/destroyable.d.ts +43 -1
  18. package/dist/destroyable.esm.js +19 -1
  19. package/dist/destroyable.esm.js.map +1 -1
  20. package/dist/destroyable.js +19 -1
  21. package/dist/destroyable.js.map +1 -1
  22. package/dist/devtools/devtools.html +9 -0
  23. package/dist/devtools/devtools.js +5 -0
  24. package/dist/devtools/devtools.js.map +1 -0
  25. package/dist/devtools/manifest.json +8 -0
  26. package/dist/devtools/panel.css +72 -0
  27. package/dist/devtools/panel.html +31 -0
  28. package/dist/devtools/panel.js +13048 -0
  29. package/dist/devtools/panel.js.map +1 -0
  30. package/dist/eventful.d.ts +10 -1
  31. package/dist/eventful.esm.js +5 -27
  32. package/dist/eventful.esm.js.map +1 -1
  33. package/dist/eventful.js +15 -37
  34. package/dist/eventful.js.map +1 -1
  35. package/dist/index.d.ts +18 -14
  36. package/dist/index.esm.js +4 -3
  37. package/dist/index.esm.js.map +1 -1
  38. package/dist/index.js +44 -5
  39. package/dist/index.js.map +1 -1
  40. package/dist/indexable.d.ts +213 -1
  41. package/dist/indexable.esm.js +203 -3
  42. package/dist/indexable.esm.js.map +1 -1
  43. package/dist/indexable.js +204 -2
  44. package/dist/indexable.js.map +1 -1
  45. package/dist/mutts.umd.js +1 -1
  46. package/dist/mutts.umd.js.map +1 -1
  47. package/dist/mutts.umd.min.js +1 -1
  48. package/dist/mutts.umd.min.js.map +1 -1
  49. package/dist/promiseChain.d.ts +10 -0
  50. package/dist/promiseChain.esm.js +6 -0
  51. package/dist/promiseChain.esm.js.map +1 -1
  52. package/dist/promiseChain.js +6 -0
  53. package/dist/promiseChain.js.map +1 -1
  54. package/dist/reactive.d.ts +774 -33
  55. package/dist/reactive.esm.js +4 -1458
  56. package/dist/reactive.esm.js.map +1 -1
  57. package/dist/reactive.js +53 -1474
  58. package/dist/reactive.js.map +1 -1
  59. package/dist/std-decorators.d.ts +35 -0
  60. package/dist/std-decorators.esm.js +36 -1
  61. package/dist/std-decorators.esm.js.map +1 -1
  62. package/dist/std-decorators.js +36 -1
  63. package/dist/std-decorators.js.map +1 -1
  64. package/docs/ai/api-reference.md +133 -0
  65. package/docs/ai/manual.md +105 -0
  66. package/docs/iterableWeak.md +646 -0
  67. package/docs/mixin.md +229 -0
  68. package/docs/reactive/advanced.md +1280 -0
  69. package/docs/reactive/collections.md +767 -0
  70. package/docs/reactive/core.md +973 -0
  71. package/docs/reactive.md +21 -2688
  72. package/package.json +18 -5
  73. package/src/decorator.ts +266 -0
  74. package/src/destroyable.ts +199 -0
  75. package/src/eventful.ts +77 -0
  76. package/src/index.d.ts +9 -0
  77. package/src/index.ts +9 -0
  78. package/src/indexable.ts +484 -0
  79. package/src/introspection.ts +59 -0
  80. package/src/iterableWeak.ts +233 -0
  81. package/src/mixins.ts +123 -0
  82. package/src/promiseChain.ts +110 -0
  83. package/src/reactive/array.ts +414 -0
  84. package/src/reactive/change.ts +134 -0
  85. package/src/reactive/debug.ts +517 -0
  86. package/src/reactive/deep-touch.ts +268 -0
  87. package/src/reactive/deep-watch-state.ts +82 -0
  88. package/src/reactive/deep-watch.ts +168 -0
  89. package/src/reactive/effect-context.ts +94 -0
  90. package/src/reactive/effects.ts +1333 -0
  91. package/src/reactive/index.ts +75 -0
  92. package/src/reactive/interface.ts +223 -0
  93. package/src/reactive/map.ts +171 -0
  94. package/src/reactive/mapped.ts +130 -0
  95. package/src/reactive/memoize.ts +107 -0
  96. package/src/reactive/non-reactive-state.ts +49 -0
  97. package/src/reactive/non-reactive.ts +43 -0
  98. package/src/reactive/project.project.md +93 -0
  99. package/src/reactive/project.ts +335 -0
  100. package/src/reactive/proxy-state.ts +27 -0
  101. package/src/reactive/proxy.ts +285 -0
  102. package/src/reactive/record.ts +196 -0
  103. package/src/reactive/register.ts +421 -0
  104. package/src/reactive/set.ts +144 -0
  105. package/src/reactive/tracking.ts +101 -0
  106. package/src/reactive/types.ts +358 -0
  107. package/src/reactive/zone.ts +208 -0
  108. package/src/std-decorators.ts +217 -0
  109. package/src/utils.ts +117 -0
  110. package/dist/chunks/decorator-8qjFb7dw.js.map +0 -1
  111. package/dist/chunks/decorator-AbRkXM5O.esm.js.map +0 -1
package/docs/reactive.md CHANGED
@@ -1,2688 +1,21 @@
1
- # Reactive Documentation
2
-
3
- ## Table of Contents
4
-
5
- - [Introduction](#introduction)
6
- - [Getting Started](#getting-started)
7
- - [Core API](#core-api)
8
- - [Effect System](#effect-system)
9
- - [Evolution Tracking](#evolution-tracking)
10
- - [Collections](#collections)
11
- - [ReactiveArray](#reactivearray)
12
- - [Class Reactivity](#class-reactivity)
13
- - [Non-Reactive System](#non-reactive-system)
14
- - [Computed Properties](#computed-properties)
15
- - [Atomic Operations](#atomic-operations)
16
- - [Advanced Patterns](#advanced-patterns)
17
- - [Debugging and Development](#debugging-and-development)
18
- - [API Reference](#api-reference)
19
-
20
- ## Introduction
21
-
22
- ### What is Reactivity?
23
-
24
- Reactivity is a programming paradigm where the system automatically tracks dependencies between data and automatically updates when data changes. This library provides a powerful, lightweight reactive system for JavaScript/TypeScript applications.
25
-
26
- ### Core Concepts
27
-
28
- - **Reactive Objects**: Plain JavaScript objects wrapped with reactive capabilities
29
- - **Effects**: Functions that automatically re-run when their dependencies change
30
- - **Dependencies**: Reactive properties that an effect depends on
31
- - **Atomic Operations**: Batching multiple state changes to execute effects only once
32
- - **Evolution Tracking**: Built-in change history for reactive objects
33
- - **Collections**: Reactive wrappers for Array, Map, Set, WeakMap, and WeakSet
34
-
35
- ### Basic Example
36
-
37
- ```typescript
38
- import { reactive, effect } from 'mutts/reactive'
39
-
40
- // Create a reactive object
41
- const user = reactive({ name: "John", age: 30 })
42
-
43
- // Create an effect that depends on user properties
44
- effect(() => {
45
- console.log(`User: ${user.name}, Age: ${user.age}`)
46
- })
47
-
48
- // When properties change, the effect automatically re-runs
49
- user.name = "Jane" // Triggers effect
50
- user.age = 25 // Triggers effect
51
- ```
52
-
53
- ## Getting Started
54
-
55
- ### Installation
56
-
57
- ```bash
58
- npm install mutts
59
- ```
60
-
61
- ### Basic Usage
62
-
63
- ```typescript
64
- import { reactive, effect } from 'mutts/reactive'
65
-
66
- // Make an object reactive
67
- const state = reactive({
68
- count: 0,
69
- message: "Hello"
70
- })
71
-
72
- // Create reactive effects
73
- effect(() => {
74
- console.log(`Count: ${state.count}`)
75
- })
76
-
77
- effect(() => {
78
- console.log(`Message: ${state.message}`)
79
- })
80
-
81
- // Changes trigger effects automatically
82
- state.count++ // Triggers first effect
83
- state.message = "Hi" // Triggers second effect
84
- ```
85
-
86
- ### Hello World Example
87
-
88
- ```typescript
89
- import { reactive, effect } from 'mutts/reactive'
90
-
91
- // Simple counter
92
- const counter = reactive({ value: 0 })
93
-
94
- effect(() => {
95
- document.body.innerHTML = `Count: ${counter.value}`
96
- })
97
-
98
- // Button click handler
99
- document.getElementById('increment').onclick = () => {
100
- counter.value++
101
- }
102
- ```
103
-
104
- ## Core API
105
-
106
- ### `reactive()`
107
-
108
- Makes an object reactive by wrapping it in a proxy.
109
-
110
- ```typescript
111
- function reactive<T extends Record<PropertyKey, any>>(target: T): T
112
- ```
113
-
114
- **Parameters:**
115
- - `target`: The object to make reactive
116
-
117
- **Returns:** A reactive proxy of the original object
118
-
119
- **Example:**
120
- ```typescript
121
- const obj = { count: 0 }
122
- const reactiveObj = reactive(obj)
123
-
124
- // reactiveObj is now reactive
125
- effect(() => {
126
- console.log(reactiveObj.count) // Tracks dependency
127
- })
128
-
129
- reactiveObj.count = 5 // Triggers effect
130
- ```
131
-
132
- **Note:** The same object will always return the same proxy instance.
133
-
134
- ### `effect()`
135
-
136
- Creates a reactive effect that automatically re-runs when dependencies change.
137
-
138
- ```typescript
139
- function effect(
140
- fn: (dep: DependencyFunction, ...args: any[]) => (ScopedCallback | undefined | void),
141
- ...args: any[]
142
- ): ScopedCallback
143
- ```
144
-
145
- **Parameters:**
146
- - `fn`: The effect function that provides dependencies and may return a cleanup function
147
- - `...args`: Additional arguments that are forwarded to the effect function
148
-
149
- **Returns:** A cleanup function to stop the effect
150
-
151
- **Example:**
152
- ```typescript
153
- const state = reactive({ count: 0, mood: 'happy' })
154
-
155
- const cleanup = effect(() => {
156
- console.log(`Count is: ${state.count}`)
157
- // Optional cleanup called before next run
158
- return () => console.log('Cleaning up...')
159
- })
160
-
161
- state.count++ // Does trigger: 1- the cleaning, 2- the effect
162
- state.mood = 'surprised' // Does not trigger the effect
163
-
164
- // Later...
165
- cleanup() // Stops the effect
166
- ```
167
-
168
- **Using effect with arguments (useful in loops):**
169
- ```typescript
170
- const items = reactive([{ id: 1 }, { id: 2 }, { id: 3 }])
171
-
172
- // Create effects in a loop, passing loop variables
173
- for (let i = 0; i < items.length; i++) {
174
- effect((dep, index) => {
175
- console.log(`Item ${index}:`, items[index])
176
- }, i) // Pass the loop variable as argument
177
- }
178
- ```
179
-
180
- ### `unwrap()`
181
-
182
- Gets the original, non-reactive object from a reactive proxy.
183
-
184
- ```typescript
185
- function unwrap<T>(proxy: T): T
186
- ```
187
-
188
- **Example:**
189
- ```typescript
190
- const original = { count: 0 }
191
- const reactive = reactive(original)
192
- const unwrapped = unwrap(reactive)
193
-
194
- console.log(unwrapped === original) // true
195
- console.log(unwrapped === reactive) // false
196
- ```
197
-
198
- ### `isReactive()`
199
-
200
- Checks if an object is a reactive proxy.
201
-
202
- ```typescript
203
- function isReactive(obj: any): boolean
204
- ```
205
-
206
- ### `isNonReactive()`
207
-
208
- Checks if an object is marked as non-reactive.
209
-
210
- ```typescript
211
- function isNonReactive(obj: any): boolean
212
- ```
213
-
214
- ## Effect System
215
-
216
- ### Basic Effects
217
-
218
- Effects are the core of the reactive system. They automatically track dependencies and re-run when those dependencies change.
219
-
220
- ```typescript
221
- const state = reactive({ count: 0, name: "John" })
222
-
223
- effect(() => {
224
- // This effect depends on state.count
225
- console.log(`Count: ${state.count}`)
226
- })
227
-
228
- // Only changing count triggers the effect
229
- state.count = 5 // Triggers effect
230
- state.name = "Jane" // Does NOT trigger effect
231
- ```
232
-
233
- ### Effect Cleanup
234
-
235
- Effects return cleanup functions that you can call to stop tracking dependencies.
236
-
237
- ```typescript
238
- const state = reactive({ count: 0 })
239
-
240
- const stopEffect = effect(() => {
241
- console.log(`Count: ${state.count}`)
242
- })
243
-
244
- // Later...
245
- stopEffect() // Stops the effect
246
-
247
- state.count = 10 // No longer triggers the effect
248
- ```
249
-
250
- ### Automatic Effect Cleanup
251
-
252
- The reactive system provides **automatic cleanup** for effects, making memory management much easier. You **do not** have to call the cleanup function in most cases, but you **may** want to, especially if your effects have side effects like timers, DOM manipulation, or event listeners.
253
-
254
- #### How Automatic Cleanup Works
255
-
256
- 1. **Parent-Child Independence**: When an effect is created inside another effect, it becomes a "child" of the parent effect. However, when the parent effect is cleaned up, child effects become independent and are NOT automatically cleaned up - they continue to run until they're garbage collected or explicitly cleaned up.
257
-
258
- 2. **Garbage Collection Cleanup**: For top-level effects (not created inside other effects) and orphaned child effects, the system uses JavaScript's garbage collection to automatically clean them up when they're no longer referenced.
259
-
260
- #### Examples
261
-
262
- **Parent-Child Independence:**
263
- ```typescript
264
- const state = reactive({ a: 1, b: 2 })
265
-
266
- const stopParent = effect(() => {
267
- state.a
268
-
269
- // Child effect - becomes independent when parent is cleaned up
270
- effect(() => {
271
- state.b
272
- return () => console.log('Child cleanup')
273
- })
274
-
275
- return () => console.log('Parent cleanup')
276
- })
277
-
278
- // Only cleans up the parent - child becomes independent
279
- stopParent() // Logs: "Parent cleanup" (child continues running)
280
- ```
281
-
282
- **Garbage Collection Cleanup:**
283
- ```typescript
284
- const state = reactive({ value: 1 })
285
-
286
- // Top-level effect - automatically cleaned up via garbage collection
287
- effect(() => {
288
- state.value
289
- return () => console.log('GC cleanup')
290
- })
291
-
292
- // No explicit cleanup needed - will be cleaned up when garbage collected
293
- ```
294
-
295
- **When You Should Store Cleanup Functions:**
296
-
297
- While cleanup is automatic, you should **store and remember** cleanup functions when your effects have side effects that need immediate cleanup:
298
-
299
- ```typescript
300
- const state = reactive({ value: 1 })
301
- const activeEffects: (() => void)[] = []
302
-
303
- // Store cleanup functions for effects with side effects
304
- for (let i = 0; i < 3; i++) {
305
- const stopEffect = effect(() => {
306
- state.value
307
-
308
- // Side effect that needs immediate cleanup
309
- const intervalId = setInterval(() => {
310
- console.log(`Timer ${i} running`)
311
- }, 1000)
312
-
313
- return () => {
314
- clearInterval(intervalId) // Prevent memory leaks
315
- }
316
- })
317
-
318
- activeEffects.push(stopEffect)
319
- }
320
-
321
- // Clean up all effects when needed
322
- activeEffects.forEach(stop => stop())
323
- ```
324
-
325
- **Key Points:**
326
-
327
- - **You do not have to call cleanup** - the system handles it automatically via garbage collection
328
- - **You may want to call cleanup** - especially for effects with side effects
329
- - **You have to store and remember cleanup** - when you need immediate control over when effects stop
330
- - **Child effects become independent** - when their parent effect is cleaned up, they continue running
331
- - **All effects use garbage collection** - as the primary cleanup mechanism
332
-
333
- ### Effect Dependencies
334
-
335
- Effects automatically track which reactive properties they access.
336
-
337
- ```typescript
338
- const state = reactive({ a: 1, b: 2, c: 3 })
339
-
340
- effect(() => {
341
- // Only tracks state.a and state.b
342
- console.log(`Sum: ${state.a + state.b}`)
343
- })
344
-
345
- // Only these changes trigger the effect
346
- state.a = 5 // Triggers effect
347
- state.b = 10 // Triggers effect
348
- state.c = 15 // Does NOT trigger effect
349
- ```
350
-
351
- ### Async Effects and the `dep` Parameter
352
-
353
- The `effect` function provides a special `dep` parameter that restores the active effect context for dependency tracking in asynchronous operations. This is crucial because async functions lose the global active effect context when they yield control.
354
-
355
- #### The Problem with Async Effects
356
-
357
- When an effect function is synchronous, reactive property access automatically tracks dependencies, however, in async functions, the active effect context is lost when the function yields control.
358
-
359
- The `dep` parameter restores the active effect context for dependency tracking.
360
-
361
- #### Understanding the active effect context
362
-
363
- The reactive system uses a global active effect variable to track which effect is currently running:
364
-
365
- ```typescript
366
- // Synchronous effect - active effect is maintained throughout
367
- effect(() => {
368
- // active effect = this effect
369
- const value = state.count // ✅ Tracked (active effect is set)
370
- // active effect = this effect (still set)
371
- const another = state.name // ✅ Tracked (active effect is still set)
372
- })
373
-
374
- // Async effect - active effect is lost after await
375
- effect(async () => {
376
- // active effect = this effect
377
- const value = state.count // ✅ Tracked (active effect is set)
378
-
379
- await someAsyncOperation() // Function exits, active effect = undefined
380
-
381
- // active effect = undefined (lost!)
382
- const another = state.name // ❌ NOT tracked (no active effect)
383
- })
384
-
385
- // Async effect with dep() - active effect is restored
386
- effect(async (dep) => {
387
- // active effect = this effect
388
- const value = state.count // ✅ Tracked (active effect is set)
389
-
390
- await someAsyncOperation() // Function exits, active effect = undefined
391
-
392
- // dep() temporarily restores active effect for the callback
393
- const another = dep(() => state.name) // ✅ Tracked (active effect restored)
394
- })
395
- ```
396
-
397
- #### Key Benefits of `dep()` in Async Effects
398
-
399
- 1. **Restored Context**: `dep()` temporarily restores the active effect context for dependency tracking
400
- 2. **Consistent Tracking**: Reactive property access works the same way before and after `await`
401
-
402
- ### Nested Effects
403
-
404
- Effects can be created inside other effects and will have separate effect scopes:
405
-
406
- ```typescript
407
- import { effect, reactive } from 'mutts/reactive'
408
-
409
- const state = reactive({ a: 0, b: 0 })
410
-
411
- const stopOuter = effect(() => {
412
- state.a
413
-
414
- // Create an inner effect with its own scope
415
- const stopInner = effect(() => {
416
- state.b
417
- })
418
-
419
- // Return cleanup function for the inner effect
420
- return stopInner
421
- })
422
- ```
423
-
424
- ### `untracked()`
425
-
426
- 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.
427
-
428
- ```typescript
429
- import { effect, untracked, reactive } from 'mutts/reactive'
430
-
431
- const state = reactive({ a: 0, b: 0 })
432
-
433
- effect(() => {
434
- state.a
435
-
436
- // Create an inner effect without tracking the creation under the outer effect
437
- let stopInner: (() => void) | undefined
438
- untracked(() => {
439
- stopInner = effect(() => {
440
- state.b
441
- })
442
- })
443
-
444
- // Optionally stop it immediately to avoid accumulating watchers
445
- stopInner && stopInner()
446
- })
447
- ```
448
-
449
- **Use cases for `untracked()`:**
450
- - Creating effects inside other effects without coupling their dependencies
451
- - Performing side effects that shouldn't trigger when dependencies change
452
- - Avoiding circular dependencies in complex reactive systems
453
-
454
- ### Effect Options and debugging
455
-
456
- Configure the reactive system behavior:
457
-
458
- ```typescript
459
- import { options as reactiveOptions } from 'mutts/reactive'
460
-
461
- // Set maximum effect chain depth
462
- reactiveOptions.maxEffectChain = 50
463
-
464
- // Set maximum deep watch traversal depth
465
- reactiveOptions.maxDeepWatchDepth = 200
466
-
467
- // Enable debug logging
468
- reactiveOptions.enter = (effect) => console.log('Entering effect:', effect)
469
- reactiveOptions.leave = (effect) => console.log('Leaving effect:', effect)
470
- reactiveOptions.chain = (caller, target) => console.log('Chaining:', caller, '->', target)
471
- ```
472
-
473
- ## Advanced Effects
474
-
475
- ### Recording Results inside Effects
476
-
477
- If you want to capture derived values from an effect, push them from inside the effect body. Use the returned cleanup to manage resources between runs.
478
-
479
- ```typescript
480
- const state = reactive({ count: 0 })
481
- const results: number[] = []
482
-
483
- const stop = effect(() => {
484
- results.push(state.count * 2)
485
- return () => {
486
- // cleanup between runs if needed
487
- }
488
- })
489
-
490
- state.count = 5 // results: [0, 10]
491
- stop()
492
- ```
493
-
494
- ### Effect Cleanup Functions
495
-
496
- Effects can return cleanup functions that run before the next execution.
497
-
498
- ```typescript
499
- const state = reactive({ count: 0 })
500
-
501
- effect(() => {
502
- const interval = setInterval(() => {
503
- console.log('Count:', state.count)
504
- }, 1000)
505
-
506
- return () => {
507
- clearInterval(interval)
508
- console.log('Cleaned up interval')
509
- }
510
- })
511
-
512
- // When state.count changes, the cleanup runs first
513
- state.count = 5
514
- ```
515
-
516
- ### Effect Lifecycle
517
-
518
- 1. **Initial Run**: Effect runs immediately when created
519
- 2. **Dependency Change**: When dependencies change, cleanup runs, then effect re-runs
520
- 3. **Manual Stop**: Calling the cleanup function stops the effect permanently
521
-
522
- ```typescript
523
- const state = reactive({ count: 0 })
524
-
525
- const stop = effect(() => {
526
- console.log('Effect running, count:', state.count)
527
- return () => console.log('Cleanup running')
528
- })
529
-
530
- console.log('Effect created')
531
-
532
- state.count = 5
533
- // Output:
534
- // Effect created
535
- // Effect running, count: 0
536
- // Cleanup running
537
- // Effect running, count: 5
538
-
539
- stop() // Effect stops permanently
540
- ```
541
-
542
- ### Effect Arguments
543
-
544
- Effects can accept additional arguments that are forwarded to the effect function. This is particularly useful in loops or when you need to pass context to the effect:
545
-
546
- ```typescript
547
- const items = reactive([
548
- { id: 1, name: 'Item 1' },
549
- { id: 2, name: 'Item 2' },
550
- { id: 3, name: 'Item 3' }
551
- ])
552
-
553
- // Create effects for each item with the item index
554
- const effectCleanups: (() => void)[] = []
555
-
556
- for (let i = 0; i < items.length; i++) {
557
- const cleanup = effect((dep, index) => {
558
- console.log(`Item ${index}:`, items[index].name)
559
-
560
- // The index is captured in the closure and passed as argument
561
- return () => console.log(`Cleaning up effect for item ${index}`)
562
- }, i)
563
-
564
- effectCleanups.push(cleanup)
565
- }
566
-
567
- // Later, clean up all effects
568
- effectCleanups.forEach(cleanup => cleanup())
569
- ```
570
-
571
- ### Watch Function
572
-
573
- The `watch` function provides a more direct way to observe changes in reactive objects. It comes in two forms:
574
-
575
- #### Watch with Value Function
576
-
577
- ```typescript
578
- const state = reactive({ count: 0, name: 'John' })
579
-
580
- const stop = watch(
581
- () => state.count, // Value function
582
- (newValue, oldValue) => {
583
- console.log(`Count changed from ${oldValue} to ${newValue}`)
584
- }
585
- )
586
-
587
- state.count = 5 // Triggers: "Count changed from 0 to 5"
588
- state.name = 'Jane' // No trigger (not watching name)
589
- ```
590
-
591
- #### Watch Object Properties
592
-
593
- The second form of `watch` allows you to watch any property change on a reactive object:
594
-
595
- ```typescript
596
- const user = reactive({
597
- name: 'John',
598
- age: 30,
599
- email: 'john@example.com'
600
- })
601
-
602
- const stop = watch(
603
- user, // The reactive object to watch
604
- () => {
605
- console.log('Any property of user changed!')
606
- console.log('Current user:', user)
607
- }
608
- )
609
-
610
- user.name = 'Jane' // Triggers the callback
611
- user.age = 31 // Triggers the callback
612
- user.email = 'jane@example.com' // Triggers the callback
613
- ```
614
-
615
- #### Use Cases
616
-
617
- **Object-level watching** is particularly useful for:
618
-
619
- - **Form validation**: Watch all form fields for changes
620
- - **Auto-save**: Save whenever any field in a document changes
621
- - **Logging**: Track all changes to a state object
622
- - **Dirty checking**: Detect if any property has been modified
623
-
624
- ```typescript
625
- const form = reactive({
626
- firstName: '',
627
- lastName: '',
628
- email: '',
629
- isValid: false
630
- })
631
-
632
- const stop = watch(form, () => {
633
- // Auto-save whenever any field changes
634
- saveForm(form)
635
-
636
- // Update validation status
637
- form.isValid = form.firstName && form.lastName && form.email
638
- })
639
-
640
- // Any change to firstName, lastName, or email will trigger auto-save
641
- form.firstName = 'John'
642
- form.lastName = 'Doe'
643
- form.email = 'john.doe@example.com'
644
- ```
645
-
646
- #### Deep Watching
647
-
648
- For both forms of `watch`, you can enable deep watching by passing `{ deep: true }` in the options:
649
-
650
- ```typescript
651
- const state = reactive({
652
- user: {
653
- name: 'John',
654
- profile: { age: 30 }
655
- }
656
- })
657
-
658
- // Deep watch - triggers on nested property changes
659
- const stop = watch(state, () => {
660
- console.log('Any nested property changed!')
661
- }, { deep: true })
662
-
663
- state.user.name = 'Jane' // Triggers
664
- state.user.profile.age = 31 // Triggers (nested change)
665
- ```
666
-
667
- **Deep Watching Behavior:**
668
-
669
- - **Unreactive objects are skipped**: Deep watching will not traverse into objects marked as unreactive using `@unreactive` or `unreactive()`
670
- - **Collections are handled specially**:
671
- - **Arrays**: All elements and length changes are tracked
672
- - **Sets**: All values are tracked (keys are not separate in Sets)
673
- - **Maps**: All values are tracked (keys are not tracked separately)
674
- - **WeakSet/WeakMap**: Cannot be deep watched (not iterable), only replacement triggers
675
- - **Circular references**: Handled safely - There is also a configurable depth limit
676
- - **Performance**: Deep watching has higher overhead than shallow watching
677
-
678
- ```typescript
679
- // Example with collections
680
- const state = reactive({
681
- items: [1, 2, 3],
682
- tags: new Set(['a', 'b']),
683
- metadata: new Map([['key1', 'value1']])
684
- })
685
-
686
- const stop = watch(state, () => {
687
- console.log('Collection changed')
688
- }, { deep: true })
689
-
690
- state.items.push(4) // Triggers
691
- state.items[0] = 10 // Triggers
692
- state.tags.add('c') // Triggers
693
- state.metadata.set('key2', 'value2') // Triggers
694
-
695
- // WeakSet/WeakMap only trigger on replacement
696
- const weakSet = new WeakSet()
697
- state.weakData = weakSet // Triggers (replacement)
698
- // Changes to weakSet contents won't trigger (not trackable)
699
- ```
700
-
701
- #### Cleanup
702
-
703
- Both forms of `watch` return a cleanup function:
704
-
705
- ```typescript
706
- const stop = watch(user, () => {
707
- console.log('User changed')
708
- })
709
-
710
- // Later, stop watching
711
- stop()
712
- ```
713
-
714
- ## Evolution Tracking
715
-
716
- ### Understanding Object Evolution
717
-
718
- The reactive system tracks how objects change over time, creating an "evolution history" that you can inspect.
719
-
720
- ```typescript
721
- import { getState } from './reactive'
722
-
723
- const obj = reactive({ count: 0 })
724
- let state = getState(obj)
725
-
726
- effect(() => {
727
- while ('evolution' in state) {
728
- console.log('Change:', state.evolution)
729
- state = state.next
730
- }
731
-
732
- console.log('Current count:', obj.count)
733
- })
734
-
735
- obj.count = 5
736
- obj.count = 10
737
- ```
738
-
739
- ### `getState()` - Accessing Change History
740
-
741
- Returns the current state object for tracking evolution.
742
-
743
- ```typescript
744
- function getState(obj: any): State
745
- ```
746
-
747
- **Returns:** A state object that does contain evolution information ( = `{}`) but will in a next evolution call if the object state has evolved.
748
-
749
- ### Evolution Types
750
-
751
- The system tracks different types of changes:
752
-
753
- - **`add`**: Property was added
754
- - **`set`**: Property value was changed
755
- - **`del`**: Property was deleted
756
- - **`bunch`**: Collection operation (array methods, map/set operations)
757
-
758
- ```typescript
759
- const obj = reactive({ count: 0 })
760
- let state = getState(obj)
761
-
762
- effect(() => {
763
- let changes = []
764
- while ('evolution' in state) {
765
- changes.push(state.evolution)
766
- state = state.next
767
- }
768
-
769
- if (changes.length > 0) {
770
- console.log('Changes since last effect:', changes)
771
- }
772
- })
773
-
774
- obj.count = 5 // { type: 'set', prop: 'count' }
775
- obj.newProp = 'test' // { type: 'add', prop: 'newProp' }
776
- delete obj.count // { type: 'del', prop: 'count' }
777
-
778
- // Array operations
779
- const array = reactive([1, 2, 3])
780
- array.push(4) // { type: 'bunch', method: 'push' }
781
- array.reverse() // { type: 'bunch', method: 'reverse' }
782
- ```
783
-
784
- ### Change History Patterns
785
-
786
- Common patterns for using evolution tracking:
787
-
788
- ```typescript
789
- // Pattern 1: Count changes
790
- let state = getState(obj)
791
- effect(() => {
792
- let changes = 0
793
- while ('evolution' in state) {
794
- changes++
795
- state = state.next
796
- }
797
-
798
- if (changes > 0) {
799
- console.log(`Detected ${changes} changes`)
800
- state = getState(obj) // Reset for next run
801
- }
802
- })
803
-
804
- // Pattern 2: Filter specific changes
805
- let state = getState(obj)
806
- effect(() => {
807
- const relevantChanges = []
808
- while ('evolution' in state) {
809
- if (state.evolution.type === 'set') {
810
- relevantChanges.push(state.evolution)
811
- }
812
- state = state.next
813
- }
814
-
815
- if (relevantChanges.length > 0) {
816
- console.log('Property updates:', relevantChanges)
817
- state = getState(obj)
818
- }
819
- })
820
- ```
821
-
822
- ## Collections
823
-
824
- ### `ReactiveMap`
825
-
826
- A reactive wrapper around JavaScript's `Map` class.
827
-
828
- ```typescript
829
- const map = reactive(new Map([['key1', 'value1']]))
830
-
831
- effect(() => {
832
- console.log('Map size:', map.size)
833
- console.log('Has key1:', map.has('key1'))
834
- })
835
-
836
- map.set('key2', 'value2') // Triggers effect
837
- map.delete('key1') // Triggers effect
838
- ```
839
-
840
- **Features:**
841
- - Tracks `size` changes
842
- - Tracks individual key operations
843
- - Tracks collection-wide operations via `allProps`
844
-
845
- ### `ReactiveWeakMap`
846
-
847
- A reactive wrapper around JavaScript's `WeakMap` class.
848
-
849
- ```typescript
850
- const weakMap = reactive(new WeakMap())
851
- const key = { id: 1 }
852
-
853
- effect(() => {
854
- console.log('Has key:', weakMap.has(key))
855
- })
856
-
857
- weakMap.set(key, 'value') // Triggers effect
858
- weakMap.delete(key) // Triggers effect
859
- ```
860
-
861
- **Features:**
862
- - Only tracks individual key operations
863
- - No `size` tracking (WeakMap limitation)
864
- - No collection-wide operations
865
-
866
- ### `ReactiveSet`
867
-
868
- A reactive wrapper around JavaScript's `Set` class.
869
-
870
- ```typescript
871
- const set = reactive(new Set([1, 2, 3]))
872
-
873
- effect(() => {
874
- console.log('Set size:', set.size)
875
- console.log('Has 1:', set.has(1))
876
- })
877
-
878
- set.add(4) // Triggers effect
879
- set.delete(1) // Triggers effect
880
- set.clear() // Triggers effect
881
- ```
882
-
883
- **Features:**
884
- - Tracks `size` changes
885
- - Tracks individual value operations
886
- - Tracks collection-wide operations
887
-
888
- ### `ReactiveWeakSet`
889
-
890
- A reactive wrapper around JavaScript's `WeakSet` class.
891
-
892
- ```typescript
893
- const weakSet = reactive(new WeakSet())
894
- const obj = { id: 1 }
895
-
896
- effect(() => {
897
- console.log('Has obj:', weakSet.has(obj))
898
- })
899
-
900
- weakSet.add(obj) // Triggers effect
901
- weakSet.delete(obj) // Triggers effect
902
- ```
903
-
904
- ### Collection-Specific Reactivity
905
-
906
- Collections provide different levels of reactivity:
907
-
908
- ```typescript
909
- const map = reactive(new Map())
910
-
911
- // Size tracking
912
- effect(() => {
913
- console.log('Map size:', map.size)
914
- })
915
-
916
- // Individual key tracking
917
- effect(() => {
918
- console.log('Value for key1:', map.get('key1'))
919
- })
920
-
921
- // Collection-wide tracking
922
- effect(() => {
923
- for (const [key, value] of map) {
924
- // This effect depends on allProps
925
- }
926
- })
927
-
928
- // Operations trigger different effects
929
- map.set('key1', 'value1') // Triggers size and key1 effects
930
- map.set('key2', 'value2') // Triggers size and allProps effects
931
- map.delete('key1') // Triggers size, key1, and allProps effects
932
- ```
933
-
934
- ## ReactiveArray
935
-
936
- ### `ReactiveArray`
937
-
938
- A reactive wrapper around JavaScript's `Array` class with full array method support.
939
-
940
- ```typescript
941
- const array = reactive([1, 2, 3])
942
-
943
- effect(() => {
944
- console.log('Array length:', array.length)
945
- console.log('First element:', array[0])
946
- })
947
-
948
- array.push(4) // Triggers effect
949
- array[0] = 10 // Triggers effect
950
- ```
951
-
952
- **Features:**
953
- - Tracks `length` changes
954
- - Tracks individual index operations
955
- - Tracks collection-wide operations via `allProps`
956
- - Supports all array methods with proper reactivity
957
-
958
- ### Array Methods
959
-
960
- All standard array methods are supported with reactivity:
961
-
962
- ```typescript
963
- const array = reactive([1, 2, 3])
964
-
965
- // Mutator methods
966
- array.push(4) // Triggers length and allProps effects
967
- array.pop() // Triggers length and allProps effects
968
- array.shift() // Triggers length and allProps effects
969
- array.unshift(0) // Triggers length and allProps effects
970
- array.splice(1, 1, 10) // Triggers length and allProps effects
971
- array.reverse() // Triggers allProps effects
972
- array.sort() // Triggers allProps effects
973
- array.fill(0) // Triggers allProps effects
974
- array.copyWithin(0, 2) // Triggers allProps effects
975
-
976
- // Accessor methods (immutable)
977
- const reversed = array.toReversed()
978
- const sorted = array.toSorted()
979
- const spliced = array.toSpliced(1, 1)
980
- const withNew = array.with(0, 100)
981
- ```
982
-
983
- ### Index Access
984
-
985
- ReactiveArray supports both positive and negative index access:
986
-
987
- ```typescript
988
- const array = reactive([1, 2, 3, 4, 5])
989
-
990
- effect(() => {
991
- console.log('First element:', array[0])
992
- console.log('Last element:', array.at(-1))
993
- })
994
-
995
- array[0] = 10 // Triggers effect
996
- array[4] = 50 // Triggers effect
997
- ```
998
-
999
- ### Length Reactivity
1000
-
1001
- The `length` property is fully reactive:
1002
-
1003
- ```typescript
1004
- const array = reactive([1, 2, 3])
1005
-
1006
- effect(() => {
1007
- console.log('Array length:', array.length)
1008
- })
1009
-
1010
- array.push(4) // Triggers effect
1011
- array.length = 2 // Triggers effect
1012
- array[5] = 10 // Triggers effect (expands array)
1013
- ```
1014
-
1015
- ### Array Evolution Tracking
1016
-
1017
- Array operations generate specific evolution events:
1018
-
1019
- ```typescript
1020
- const array = reactive([1, 2, 3])
1021
- let state = getState(array)
1022
-
1023
- effect(() => {
1024
- while ('evolution' in state) {
1025
- console.log('Array change:', state.evolution)
1026
- state = state.next
1027
- }
1028
- })
1029
-
1030
- array.push(4) // { type: 'bunch', method: 'push' }
1031
- array[0] = 10 // { type: 'set', prop: 0 }
1032
- array[5] = 20 // { type: 'add', prop: 5 }
1033
- ```
1034
-
1035
- ### Array-Specific Reactivity Patterns
1036
-
1037
- ```typescript
1038
- const array = reactive([1, 2, 3])
1039
-
1040
- // Track specific indices
1041
- effect(() => {
1042
- console.log('First two elements:', array[0], array[1])
1043
- })
1044
-
1045
- // Track length changes
1046
- effect(() => {
1047
- console.log('Array size changed to:', array.length)
1048
- })
1049
-
1050
- // Track all elements (via iteration)
1051
- effect(() => {
1052
- for (const item of array) {
1053
- // This effect depends on allProps
1054
- }
1055
- })
1056
-
1057
- // Track specific array methods
1058
- effect(() => {
1059
- const lastElement = array.at(-1)
1060
- console.log('Last element:', lastElement)
1061
- })
1062
- ```
1063
-
1064
- ### Performance Considerations
1065
-
1066
- ReactiveArray is optimized for common array operations:
1067
-
1068
- ```typescript
1069
- // Efficient: Direct index access
1070
- effect(() => {
1071
- console.log(array[0]) // Only tracks index 0
1072
- })
1073
-
1074
- // Efficient: Length tracking
1075
- effect(() => {
1076
- console.log(array.length) // Only tracks length
1077
- })
1078
-
1079
- // Less efficient: Iteration tracks all elements
1080
- effect(() => {
1081
- array.forEach(item => console.log(item)) // Tracks allProps
1082
- })
1083
- ```
1084
-
1085
- ## Class Reactivity
1086
-
1087
- ### `@reactive` Decorator
1088
-
1089
- The `@reactive` decorator makes class instances automatically reactive. This is the recommended approach for adding reactivity to classes.
1090
-
1091
- ```typescript
1092
- import { reactive } from 'mutts/reactive'
1093
-
1094
- @reactive
1095
- class User {
1096
- name: string
1097
- age: number
1098
-
1099
- constructor(name: string, age: number) {
1100
- this.name = name
1101
- this.age = age
1102
- }
1103
-
1104
- updateAge(newAge: number) {
1105
- this.age = newAge
1106
- }
1107
- }
1108
-
1109
- const user = new User("John", 30)
1110
-
1111
- effect(() => {
1112
- console.log(`User: ${user.name}, Age: ${user.age}`)
1113
- })
1114
-
1115
- user.updateAge(31) // Triggers effect
1116
- user.name = "Jane" // Triggers effect
1117
- ```
1118
-
1119
- ### Functional Syntax
1120
-
1121
- You can also use the functional syntax for making classes reactive:
1122
-
1123
- ```typescript
1124
- import { reactive } from 'mutts/reactive'
1125
-
1126
- class User {
1127
- name: string
1128
- age: number
1129
-
1130
- constructor(name: string, age: number) {
1131
- this.name = name
1132
- this.age = age
1133
- }
1134
-
1135
- updateAge(newAge: number) {
1136
- this.age = newAge
1137
- }
1138
- }
1139
-
1140
- const ReactiveUser = reactive(User)
1141
- const user = new ReactiveUser("John", 30)
1142
-
1143
- effect(() => {
1144
- console.log(`User: ${user.name}, Age: ${user.age}`)
1145
- })
1146
-
1147
- user.updateAge(31) // Triggers effect
1148
- user.name = "Jane" // Triggers effect
1149
- ```
1150
-
1151
- ### `ReactiveBase` for Complex Inheritance
1152
-
1153
- For complex inheritance trees, especially when you need to solve constructor reactivity issues, extend `ReactiveBase`:
1154
-
1155
- ```typescript
1156
- import { ReactiveBase, reactive } from 'mutts/reactive'
1157
-
1158
- class GameObject extends ReactiveBase {
1159
- id = 'game-object'
1160
- position = { x: 0, y: 0 }
1161
- }
1162
-
1163
- class Entity extends GameObject {
1164
- health = 100
1165
- }
1166
-
1167
- @reactive
1168
- class Player extends Entity {
1169
- name = 'Player'
1170
- level = 1
1171
- }
1172
-
1173
- const player = new Player()
1174
-
1175
- effect(() => {
1176
- console.log(`Player ${player.name} at (${player.position.x}, ${player.position.y})`)
1177
- })
1178
-
1179
- player.position.x = 10 // Triggers effect
1180
- player.health = 80 // Triggers effect
1181
- ```
1182
-
1183
- **Advantages of `ReactiveBase`:**
1184
-
1185
- 1. **Constructor Reactivity**: Solves the issue where `this` in the constructor is not yet reactive
1186
- 2. **Inheritance Safety**: Prevents reactivity from being added to prototype chains in complex inheritance trees
1187
- 3. **No Side Effects**: The base class itself has no effect - it only enables proper reactivity when combined with `@reactive`
1188
-
1189
- ### Choosing the Right Approach
1190
-
1191
- **Use `@reactive` decorator when:**
1192
- - You have simple classes without complex inheritance
1193
- - You want the cleanest, most modern syntax
1194
- - You don't need to modify or use `this` in the constructor
1195
-
1196
- **Use `ReactiveBase` + `@reactive` when:**
1197
- - You have complex inheritance trees (like game objects, UI components)
1198
- - You need to modify or use `this` in the constructor
1199
- - You want to prevent reactivity from being added to prototype chains
1200
-
1201
- ### Making Existing Class Instances Reactive
1202
-
1203
- You can also make existing class instances reactive:
1204
-
1205
- ```typescript
1206
- class Counter {
1207
- count = 0
1208
-
1209
- increment() {
1210
- this.count++
1211
- }
1212
- }
1213
-
1214
- const counter = new Counter()
1215
- const reactiveCounter = reactive(counter)
1216
-
1217
- effect(() => {
1218
- console.log('Count:', reactiveCounter.count)
1219
- })
1220
-
1221
- reactiveCounter.increment() // Triggers effect
1222
- ```
1223
-
1224
- ### Method Reactivity
1225
-
1226
- Methods that modify properties automatically trigger effects:
1227
-
1228
- ```typescript
1229
- @reactive
1230
- class ShoppingCart {
1231
- items: string[] = []
1232
-
1233
- addItem(item: string) {
1234
- this.items.push(item)
1235
- }
1236
-
1237
- removeItem(item: string) {
1238
- const index = this.items.indexOf(item)
1239
- if (index > -1) {
1240
- this.items.splice(index, 1)
1241
- }
1242
- }
1243
- }
1244
-
1245
- const cart = new ShoppingCart()
1246
-
1247
- effect(() => {
1248
- console.log('Cart items:', cart.items)
1249
- })
1250
-
1251
- cart.addItem('Apple') // Triggers effect
1252
- cart.removeItem('Apple') // Triggers effect
1253
- ```
1254
-
1255
- ### Inheritance Support
1256
-
1257
- The `@reactive` decorator works with inheritance:
1258
-
1259
- ```typescript
1260
- @reactive
1261
- class Animal {
1262
- species: string
1263
-
1264
- constructor(species: string) {
1265
- this.species = species
1266
- }
1267
- }
1268
-
1269
- class Dog extends Animal {
1270
- breed: string
1271
-
1272
- constructor(breed: string) {
1273
- super('Canis')
1274
- this.breed = breed
1275
- }
1276
- }
1277
-
1278
- const dog = new Dog('Golden Retriever')
1279
-
1280
- effect(() => {
1281
- console.log(`${dog.species}: ${dog.breed}`)
1282
- })
1283
-
1284
- dog.breed = 'Labrador' // Triggers effect
1285
- ```
1286
-
1287
- **Note**: When using inheritance with the `@reactive` decorator, apply it to the base class. The decorator will automatically handle inheritance properly.
1288
-
1289
- ## Non-Reactive System
1290
-
1291
- ### `unreactive()`
1292
-
1293
- Marks objects or classes as non-reactive, preventing them from being wrapped.
1294
-
1295
- ```typescript
1296
- function unreactive<T>(target: T): T
1297
- function unreactive(target: Constructor<T>): Constructor<T>
1298
- ```
1299
-
1300
- **Examples:**
1301
-
1302
- ```typescript
1303
- // Mark individual object as non-reactive
1304
- const obj = { count: 0 }
1305
- unreactive(obj)
1306
- const reactiveObj = reactive(obj) // Returns obj unchanged
1307
-
1308
- // Mark entire class as non-reactive
1309
- class Utility {
1310
- static helper() { return 'help' }
1311
- }
1312
- unreactive(Utility)
1313
- const instance = new Utility()
1314
- const reactiveInstance = reactive(instance) // Returns instance unchanged
1315
- ```
1316
-
1317
- ### `@unreactive` Decorator
1318
-
1319
- Mark class properties as non-reactive.
1320
-
1321
- ```typescript
1322
- @reactive
1323
- class User {
1324
- @unreactive
1325
- id: string = 'user-123'
1326
-
1327
- name: string = 'John'
1328
- age: number = 30
1329
- }
1330
-
1331
- const user = new User()
1332
-
1333
- effect(() => {
1334
- console.log(user.name, user.age) // Tracks these
1335
- console.log(user.id) // Does NOT track this
1336
- })
1337
-
1338
- user.name = 'Jane' // Triggers effect
1339
- user.id = 'new-id' // Does NOT trigger effect
1340
- ```
1341
-
1342
- ### Non-Reactive Classes
1343
-
1344
- Classes marked as non-reactive bypass the reactive system entirely:
1345
-
1346
- ```typescript
1347
- class Config {
1348
- apiUrl: string = 'https://api.example.com'
1349
- timeout: number = 5000
1350
- }
1351
-
1352
- unreactive(Config)
1353
- ```
1354
- -or-
1355
- ```typescript
1356
- @unreactive
1357
- class Config {
1358
- apiUrl: string = 'https://api.example.com'
1359
- timeout: number = 5000
1360
- }
1361
-
1362
- const ReactiveConfig = reactive(Config)
1363
- const config = new ReactiveConfig()
1364
-
1365
- effect(() => {
1366
- console.log('Config:', config.apiUrl, config.timeout)
1367
- })
1368
-
1369
- // These changes won't trigger effects
1370
- config.apiUrl = 'https://new-api.example.com'
1371
- config.timeout = 10000
1372
- ```
1373
-
1374
- ### Making Special Reactive Objects Non-Reactive
1375
-
1376
- Special reactive objects (Arrays, Maps, Sets, WeakMaps, WeakSets) can also be made non-reactive:
1377
-
1378
- ```typescript
1379
- // Make individual reactive collections non-reactive
1380
- const array = reactive([1, 2, 3])
1381
- unreactive(array) // array is no longer reactive
1382
-
1383
- const map = reactive(new Map([['key', 'value']]))
1384
- unreactive(map) // map is no longer reactive
1385
-
1386
- const set = reactive(new Set([1, 2, 3]))
1387
- unreactive(set) // set is no longer reactive
1388
- ```
1389
-
1390
- **Making reactive collection classes non-reactive:**
1391
-
1392
- ```typescript
1393
- // Make the entire ReactiveArray class non-reactive
1394
- unreactive(ReactiveArray)
1395
-
1396
- // Now all ReactiveArray instances will be non-reactive
1397
- const array = reactive([1, 2, 3]) // Returns non-reactive array
1398
- const reactiveArray = new ReactiveArray([1, 2, 3]) // Non-reactive instance
1399
-
1400
- // Make other reactive collection classes non-reactive
1401
- unreactive(ReactiveMap)
1402
- unreactive(ReactiveSet)
1403
- unreactive(ReactiveWeakMap)
1404
- unreactive(ReactiveWeakSet)
1405
- ```
1406
-
1407
- **Use cases for non-reactive collections:**
1408
- - Large datasets that don't need reactivity
1409
- - Performance-critical operations
1410
- - Static configuration data
1411
- - Temporary data structures
1412
-
1413
- ### Performance Considerations
1414
-
1415
- Non-reactive objects can improve performance:
1416
-
1417
- ```typescript
1418
- // Good: Mark large, rarely-changing objects as non-reactive
1419
- const config = unreactive({
1420
- apiEndpoints: { /* large config object */ },
1421
- featureFlags: { /* many flags */ }
1422
- })
1423
-
1424
- // Good: Mark utility classes as non-reactive
1425
- class MathUtils {
1426
- static PI = 3.14159
1427
- static square(x: number) { return x * x }
1428
- }
1429
- unreactive(MathUtils)
1430
-
1431
- // Good: Mark properties that don't need reactivity
1432
- class User {
1433
- @unreactive
1434
- metadata: any = {} // Large metadata object
1435
-
1436
- name: string = 'John' // This should be reactive
1437
- }
1438
- ```
1439
-
1440
- ## Computed Properties
1441
-
1442
- ### `@computed` Decorator
1443
-
1444
- Creates computed properties that cache their values and only recompute when dependencies change.
1445
-
1446
- ```typescript
1447
- @reactive
1448
- class Calculator {
1449
- @computed
1450
- get area() {
1451
- return this.width * this.height
1452
- }
1453
-
1454
- width: number = 10
1455
- height: number = 5
1456
- }
1457
-
1458
- const calc = new Calculator()
1459
-
1460
- effect(() => {
1461
- console.log('Area:', calc.area)
1462
- })
1463
-
1464
- calc.width = 20 // Triggers effect, recomputes area
1465
- calc.height = 10 // Triggers effect, recomputes area
1466
- ```
1467
-
1468
- ### Computed Functions
1469
-
1470
- Create computed values outside of classes:
1471
-
1472
- ```typescript
1473
- const state = reactive({ a: 1, b: 2 })
1474
-
1475
- const sum = computed(() => state.a + state.b)
1476
- const product = computed(() => state.a * state.b)
1477
-
1478
- effect(() => {
1479
- console.log('Sum:', sum, 'Product:', product)
1480
- })
1481
-
1482
- state.a = 5 // Both computed values update
1483
- ```
1484
-
1485
- ### Caching and Invalidation
1486
-
1487
- Computed values are cached until their dependencies change:
1488
-
1489
- ```typescript
1490
- const state = reactive({ x: 1, y: 2 })
1491
-
1492
- let computeCount = 0
1493
- const expensive = computed(() => {
1494
- computeCount++
1495
- console.log('Computing expensive value...')
1496
- return Math.pow(state.x, state.y)
1497
- })
1498
-
1499
- console.log('First access:', expensive) // Computes once
1500
- console.log('Second access:', expensive) // Uses cached value
1501
- console.log('Compute count:', computeCount) // 1
1502
-
1503
- state.x = 2 // Invalidates cache
1504
- console.log('After change:', expensive) // Recomputes
1505
- console.log('Compute count:', computeCount) // 2
1506
- ```
1507
-
1508
- ### Computed vs Effects
1509
-
1510
- Choose between computed and effects based on your needs:
1511
-
1512
- ```typescript
1513
- const state = reactive({ count: 0 })
1514
-
1515
- // Use computed when you need a value
1516
- const doubled = computed(() => state.count * 2)
1517
-
1518
- // Use effect when you need side effects
1519
- effect(() => {
1520
- console.log('Count doubled:', doubled)
1521
- document.title = `Count: ${state.count}`
1522
- })
1523
-
1524
- state.count = 5
1525
- // doubled becomes 10
1526
- // effect logs and updates title
1527
- ```
1528
-
1529
- ## Atomic Operations
1530
-
1531
- The `atomic` function and `@atomic` decorator are powerful tools for batching reactive effects. When applied to a method or function, they ensure that all effects triggered by reactive state changes within that scope are batched together and executed only once, rather than after each individual change.
1532
-
1533
- ### Overview
1534
-
1535
- In reactive systems, each state change typically triggers its dependent effects immediately. However, when you need to make multiple related changes as a single unit, this can lead to:
1536
-
1537
- - Multiple unnecessary effect executions
1538
- - Inconsistent intermediate states being observed
1539
- - Performance overhead from redundant computations
1540
-
1541
- The `atomic` function and `@atomic` decorator solve this by deferring effect execution until the function or method completes, treating all changes as a single atomic operation.
1542
-
1543
- ### Basic Usage
1544
-
1545
- #### Decorator Syntax
1546
-
1547
- ```typescript
1548
- import { reactive, effect, atomic } from 'mutts'
1549
-
1550
- const state = reactive({ a: 0, b: 0, c: 0 })
1551
- let effectCount = 0
1552
-
1553
- effect(() => {
1554
- effectCount++
1555
- console.log(`Effect ran: a=${state.a}, b=${state.b}, c=${state.c}`)
1556
- })
1557
-
1558
- class StateManager {
1559
- @atomic
1560
- updateAll() {
1561
- state.a = 1
1562
- state.b = 2
1563
- state.c = 3
1564
- }
1565
- }
1566
-
1567
- const manager = new StateManager()
1568
- manager.updateAll()
1569
-
1570
- // Output:
1571
- // Effect ran: a=0, b=0, c=0 (initial run)
1572
- // Effect ran: a=1, b=2, c=3 (only one additional run despite 3 changes)
1573
- ```
1574
-
1575
- #### Function Syntax
1576
-
1577
- For standalone functions or when you need more flexibility, you can use the `atomic` function directly:
1578
-
1579
- ```typescript
1580
- import { reactive, effect, atomic } from 'mutts'
1581
-
1582
- const state = reactive({ a: 0, b: 0, c: 0 })
1583
- let effectCount = 0
1584
-
1585
- effect(() => {
1586
- effectCount++
1587
- console.log(`Effect ran: a=${state.a}, b=${state.b}, c=${state.c}`)
1588
- })
1589
-
1590
- // Using atomic function
1591
- atomic(() => {
1592
- state.a = 1
1593
- state.b = 2
1594
- state.c = 3
1595
- })
1596
-
1597
- // Output:
1598
- // Effect ran: a=0, b=0, c=0 (initial run)
1599
- // Effect ran: a=1, b=2, c=3 (only one additional run despite 3 changes)
1600
- ```
1601
-
1602
- #### Returning Values
1603
-
1604
- The atomic function can return values:
1605
-
1606
- ```typescript
1607
- const result = atomic(() => {
1608
- state.a = 10
1609
- state.b = 20
1610
- return state.a + state.b
1611
- })
1612
-
1613
- console.log(result) // 30
1614
- ```
1615
-
1616
- #### Atomic Method Return Values
1617
-
1618
- The `@atomic` decorator also supports return values from methods:
1619
-
1620
- ```typescript
1621
- @reactive
1622
- class Calculator {
1623
- @atomic
1624
- updateAndCalculate(a: number, b: number) {
1625
- this.a = a
1626
- this.b = b
1627
- return { sum: a + b, product: a * b }
1628
- }
1629
- }
1630
-
1631
- const calc = new Calculator()
1632
- const result = calc.updateAndCalculate(5, 10)
1633
- console.log(result) // { sum: 15, product: 50 }
1634
- ```
1635
-
1636
- **Key Points:**
1637
- - Atomic methods can return any value type (primitives, objects, functions)
1638
- - Return values are computed during method execution
1639
- - Effects are batched until the method completes, regardless of return values
1640
- - Both read-only and state-modifying methods can return values
1641
-
1642
- ```typescript
1643
- @reactive
1644
- class DataProcessor {
1645
- @atomic
1646
- processData(items: Item[]) {
1647
- // Read-only method with return value
1648
- const total = items.reduce((sum, item) => sum + item.value, 0)
1649
- return total
1650
- }
1651
-
1652
- @atomic
1653
- updateAndProcess(items: Item[]) {
1654
- // State-modifying method with return value
1655
- this.items = items
1656
- this.processedCount = items.length
1657
- this.lastProcessed = new Date()
1658
-
1659
- return {
1660
- count: items.length,
1661
- total: items.reduce((sum, item) => sum + item.value, 0)
1662
- }
1663
- }
1664
- }
1665
- ```
1666
-
1667
- ### When to Use Each Approach
1668
-
1669
- #### Use `@atomic` Decorator When:
1670
- - You're working with class methods
1671
- - You want to declare atomic behavior at the method level
1672
- - You prefer declarative syntax
1673
- - The method is part of a reactive class
1674
-
1675
- ```typescript
1676
- @reactive
1677
- class TodoManager {
1678
- @atomic
1679
- addTodo(text: string) {
1680
- this.todos.push({ id: Date.now(), text, completed: false })
1681
- this.updateStats()
1682
- }
1683
- }
1684
- ```
1685
-
1686
- #### Use `atomic()` Function When:
1687
- - You need atomic behavior for standalone functions
1688
- - You're working with functional code
1689
- - You need to conditionally apply atomic behavior
1690
- - You're working outside of classes
1691
-
1692
- ```typescript
1693
- // Conditional atomic behavior
1694
- const updateState = (shouldBatch: boolean) => {
1695
- const updateFn = () => {
1696
- state.a = 1
1697
- state.b = 2
1698
- }
1699
-
1700
- return shouldBatch ? atomic(updateFn) : updateFn()
1701
- }
1702
-
1703
- // Functional approach
1704
- const processItems = (items: Item[]) => {
1705
- return atomic(() => {
1706
- items.forEach(item => state.items.set(item.id, item))
1707
- state.count = state.items.size
1708
- return state.count
1709
- })
1710
- }
1711
- ```
1712
-
1713
- ### Key Behaviors
1714
-
1715
- #### Immediate Execution, Batched Effects
1716
-
1717
- The decorated method executes immediately, but effects are deferred until completion:
1718
-
1719
- ```typescript
1720
- class TestClass {
1721
- @atomic
1722
- updateAndLog() {
1723
- console.log('Method starts')
1724
- state.a = 1
1725
- console.log('After setting a')
1726
- state.b = 2
1727
- console.log('After setting b')
1728
- console.log('Method ends')
1729
- }
1730
- }
1731
-
1732
- // Execution order:
1733
- // Method starts
1734
- // After setting a
1735
- // After setting b
1736
- // Method ends
1737
- // Effect runs with final values
1738
- ```
1739
-
1740
- #### Nested Atomic Methods
1741
-
1742
- Multiple atomic methods can be nested, and all effects are batched at the outermost level:
1743
-
1744
- ```typescript
1745
- class TestClass {
1746
- @atomic
1747
- updateA() {
1748
- state.a = 1
1749
- }
1750
-
1751
- @atomic
1752
- updateB() {
1753
- state.b = 2
1754
- }
1755
-
1756
- @atomic
1757
- updateAll() {
1758
- this.updateA()
1759
- this.updateB()
1760
- state.c = 3
1761
- }
1762
- }
1763
-
1764
- // Calling updateAll() will batch all effects from updateA, updateB, and the direct assignment
1765
- ```
1766
-
1767
- #### Cascading Effects
1768
-
1769
- Effects that trigger other effects are also batched within atomic methods:
1770
-
1771
- ```typescript
1772
- // Create cascading effects
1773
- effect(() => {
1774
- state.b = state.a * 2
1775
- })
1776
- effect(() => {
1777
- state.c = state.b + 1
1778
- })
1779
-
1780
- class TestClass {
1781
- @atomic
1782
- triggerCascade() {
1783
- state.a = 5 // This triggers the cascade
1784
- }
1785
- }
1786
-
1787
- // All cascading effects are batched together
1788
- ```
1789
-
1790
- ### Advanced Usage
1791
-
1792
- #### Working with Reactive Classes
1793
-
1794
- The `@atomic` decorator works seamlessly with `@reactive` classes:
1795
-
1796
- ```typescript
1797
- @reactive
1798
- class Counter {
1799
- value = 0
1800
- multiplier = 1
1801
-
1802
- @atomic
1803
- updateBoth(newValue: number, newMultiplier: number) {
1804
- this.value = newValue
1805
- this.multiplier = newMultiplier
1806
- }
1807
- }
1808
-
1809
- const counter = new Counter()
1810
- let effectCount = 0
1811
-
1812
- effect(() => {
1813
- effectCount++
1814
- console.log(`Counter: ${counter.value} * ${counter.multiplier}`)
1815
- })
1816
-
1817
- counter.updateBoth(5, 2)
1818
- // Effect runs only once despite two property changes
1819
- ```
1820
-
1821
- #### Complex Data Structures
1822
-
1823
- Atomic methods work with arrays, maps, sets, and other complex data structures:
1824
-
1825
- ```typescript
1826
- const state = reactive({
1827
- items: [1, 2, 3],
1828
- metadata: new Map([['count', 3]]),
1829
- tags: new Set(['active'])
1830
- })
1831
-
1832
- class DataManager {
1833
- @atomic
1834
- addItem(item: number) {
1835
- state.items.push(item)
1836
- state.metadata.set('count', state.items.length)
1837
- state.tags.add('modified')
1838
- }
1839
-
1840
- @atomic
1841
- clearAll() {
1842
- state.items.length = 0
1843
- state.metadata.clear()
1844
- state.tags.clear()
1845
- }
1846
- }
1847
- ```
1848
-
1849
- #### Error Handling
1850
-
1851
- If an atomic method throws an error, effects are still executed for the changes that were made before the error:
1852
-
1853
- ```typescript
1854
- class TestClass {
1855
- @atomic
1856
- updateWithError() {
1857
- state.a = 1
1858
- throw new Error('Something went wrong')
1859
- // state.b = 2 // This line never executes
1860
- }
1861
- }
1862
-
1863
- // Effect will run once for the change to state.a
1864
- // state.b remains unchanged due to the error
1865
- ```
1866
-
1867
- #### Async Operations
1868
-
1869
- Atomic methods can be async, but effects are still batched:
1870
-
1871
- ```typescript
1872
- class TestClass {
1873
- @atomic
1874
- async updateAsync() {
1875
- state.a = 1
1876
- await someAsyncOperation()
1877
- state.b = 2
1878
- }
1879
- }
1880
-
1881
- // Effects are batched even with async operations
1882
- ```
1883
-
1884
- ### Performance Benefits
1885
-
1886
- #### Reduced Effect Executions
1887
-
1888
- Without `atomic`:
1889
- ```typescript
1890
- // This would trigger the effect 3 times
1891
- state.a = 1 // Effect runs
1892
- state.b = 2 // Effect runs
1893
- state.c = 3 // Effect runs
1894
- ```
1895
-
1896
- With `atomic`:
1897
- ```typescript
1898
- @atomic
1899
- updateAll() {
1900
- state.a = 1
1901
- state.b = 2
1902
- state.c = 3
1903
- }
1904
- // Effect runs only once
1905
- ```
1906
-
1907
- #### Consistent State
1908
-
1909
- Atomic operations ensure that effects always see a consistent state:
1910
-
1911
- ```typescript
1912
- effect(() => {
1913
- // This will never see inconsistent intermediate states
1914
- if (state.a > 0 && state.b > 0) {
1915
- console.log('Both values are positive')
1916
- }
1917
- })
1918
-
1919
- @atomic
1920
- updateBoth() {
1921
- state.a = 1 // Effect doesn't run yet
1922
- state.b = 2 // Effect doesn't run yet
1923
- // Effect runs once with both values updated
1924
- }
1925
- ```
1926
-
1927
- ### Best Practices
1928
-
1929
- #### Use for Related Changes
1930
-
1931
- Apply `atomic` to methods that make logically related changes:
1932
-
1933
- ```typescript
1934
- // Good: Related user profile updates
1935
- @atomic
1936
- updateProfile(name: string, age: number, email: string) {
1937
- this.name = name
1938
- this.age = age
1939
- this.email = email
1940
- }
1941
-
1942
- // Good: Complex state initialization
1943
- @atomic
1944
- initialize() {
1945
- this.loading = false
1946
- this.data = fetchedData
1947
- this.error = null
1948
- this.lastUpdated = new Date()
1949
- }
1950
- ```
1951
-
1952
- #### Combine with Other Decorators
1953
-
1954
- The `@atomic` decorator works well with other decorators:
1955
-
1956
- ```typescript
1957
- @reactive
1958
- class UserManager {
1959
- @atomic
1960
- updateUser(id: string, updates: Partial<User>) {
1961
- this.users.set(id, { ...this.users.get(id), ...updates })
1962
- this.lastModified = new Date()
1963
- }
1964
- }
1965
- ```
1966
-
1967
- #### Consider Performance
1968
-
1969
- For methods that make many changes, `atomic` provides significant performance benefits:
1970
-
1971
- ```typescript
1972
- @atomic
1973
- updateManyItems(items: Item[]) {
1974
- for (const item of items) {
1975
- this.items.set(item.id, item)
1976
- }
1977
- this.count = this.items.size
1978
- this.lastUpdate = new Date()
1979
- }
1980
- // Without @atomic: effect would run for each item + count + timestamp
1981
- // With @atomic: effect runs only once
1982
- ```
1983
-
1984
- ### Limitations
1985
-
1986
- - **Method-only**: The decorator only works on class methods, not standalone functions (use `atomic()` function instead)
1987
- - **Synchronous batching**: Effects are batched until the method completes, but async operations within the method don't affect the batching
1988
- - **Error handling**: If a method throws, effects still run for changes made before the error
1989
-
1990
- ### Integration
1991
-
1992
- The `atomic` function and `@atomic` decorator integrate seamlessly with:
1993
-
1994
- - `@reactive` classes
1995
- - `@unreactive` property marking
1996
- - `effect()` functions
1997
- - `computed()` values
1998
- - Native collection types (Array, Map, Set, etc.)
1999
-
2000
- ### Complete Example
2001
-
2002
- ```typescript
2003
- import { reactive, effect, atomic } from 'mutts'
2004
-
2005
- @reactive
2006
- class TodoManager {
2007
- todos: Todo[] = []
2008
- filter: 'all' | 'active' | 'completed' = 'all'
2009
- loading = false
2010
-
2011
- @atomic
2012
- addTodo(text: string) {
2013
- const todo: Todo = {
2014
- id: Date.now().toString(),
2015
- text,
2016
- completed: false,
2017
- createdAt: new Date()
2018
- }
2019
- this.todos.push(todo)
2020
- this.updateStats()
2021
- }
2022
-
2023
- @atomic
2024
- toggleTodo(id: string) {
2025
- const todo = this.todos.find(t => t.id === id)
2026
- if (todo) {
2027
- todo.completed = !todo.completed
2028
- this.updateStats()
2029
- }
2030
- }
2031
-
2032
- @atomic
2033
- setFilter(filter: 'all' | 'active' | 'completed') {
2034
- this.filter = filter
2035
- this.loading = false
2036
- }
2037
-
2038
- private updateStats() {
2039
- // This method is called from within atomic methods
2040
- // Effects will be batched until the calling atomic method completes
2041
- const activeCount = this.todos.filter(t => !t.completed).length
2042
- const completedCount = this.todos.length - activeCount
2043
-
2044
- // Update derived state
2045
- this.activeCount = activeCount
2046
- this.completedCount = completedCount
2047
- this.allCompleted = completedCount === this.todos.length && this.todos.length > 0
2048
- }
2049
- }
2050
-
2051
- // Usage
2052
- const todoManager = new TodoManager()
2053
-
2054
- effect(() => {
2055
- console.log(`Active: ${todoManager.activeCount}, Completed: ${todoManager.completedCount}`)
2056
- })
2057
-
2058
- // Adding a todo triggers updateStats, but effect runs only once
2059
- todoManager.addTodo('Learn MutTs atomic operations')
2060
-
2061
- // Toggling a todo also triggers updateStats, effect runs only once
2062
- todoManager.toggleTodo('some-id')
2063
- ```
2064
-
2065
- This example demonstrates how `atomic` ensures that complex state updates are treated as single, consistent operations, improving both performance and reliability.
2066
-
2067
- ## Advanced Patterns
2068
-
2069
- ### Custom Reactive Objects
2070
-
2071
- Create custom reactive objects with specialized behavior:
2072
-
2073
- ```typescript
2074
- class ReactiveArray<T> {
2075
- private items: T[] = []
2076
-
2077
- push(item: T) {
2078
- this.items.push(item)
2079
- touched(this, 'length')
2080
- touched(this, 'allProps')
2081
- }
2082
-
2083
- get length() {
2084
- dependant(this, 'length')
2085
- return this.items.length
2086
- }
2087
-
2088
- get(index: number) {
2089
- dependant(this, index)
2090
- return this.items[index]
2091
- }
2092
- }
2093
-
2094
- const array = new ReactiveArray<number>()
2095
- effect(() => {
2096
- console.log('Array length:', array.length)
2097
- })
2098
-
2099
- array.push(1) // Triggers effect
2100
- ```
2101
-
2102
- ### Native Reactivity Registration
2103
-
2104
- Register custom reactive classes for automatic wrapping:
2105
-
2106
- ```typescript
2107
- import { registerNativeReactivity } from 'mutts/reactive'
2108
-
2109
- class CustomMap<K, V> {
2110
- private data = new Map<K, V>()
2111
-
2112
- set(key: K, value: V) {
2113
- this.data.set(key, value)
2114
- // Custom reactivity logic
2115
- }
2116
-
2117
- get(key: K) {
2118
- return this.data.get(key)
2119
- }
2120
- }
2121
-
2122
- class ReactiveCustomMap<K, V> extends CustomMap<K, V> {
2123
- // Reactive wrapper implementation
2124
- }
2125
-
2126
- registerNativeReactivity(CustomMap, ReactiveCustomMap)
2127
-
2128
- // Now CustomMap instances are automatically wrapped
2129
- const customMap = reactive(new CustomMap())
2130
- ```
2131
-
2132
- ### Memory Management
2133
-
2134
- The reactive system uses WeakMaps to avoid memory leaks:
2135
-
2136
- ```typescript
2137
- // Objects can be garbage collected when no longer referenced
2138
- let obj = { data: 'large object' }
2139
- const reactiveObj = reactive(obj)
2140
-
2141
- effect(() => {
2142
- console.log(reactiveObj.data)
2143
- })
2144
-
2145
- // Remove reference
2146
- obj = null
2147
- reactiveObj = null
2148
-
2149
- // The original object and its reactive wrapper can be GC'd
2150
- ```
2151
-
2152
- ### Performance Optimization
2153
-
2154
- Optimize reactive performance:
2155
-
2156
- ```typescript
2157
- // 1. Use non-reactive for static data
2158
- const staticConfig = unreactive({
2159
- apiUrl: 'https://api.example.com',
2160
- version: '1.0.0'
2161
- })
2162
-
2163
- // 2. Batch changes when possible
2164
- const state = reactive({ a: 1, b: 2, c: 3 })
2165
-
2166
- // Instead of:
2167
- state.a = 10
2168
- state.b = 20
2169
- state.c = 30
2170
-
2171
- // Consider batching or using a single update method
2172
-
2173
- // 3. Avoid unnecessary reactivity
2174
- @reactive
2175
- class User {
2176
- @unreactive
2177
- private internalId = crypto.randomUUID() // Never changes
2178
-
2179
- name: string = 'John' // Changes, should be reactive
2180
- }
2181
-
2182
- // 4. Use appropriate collection types
2183
- const smallSet = new ReactiveSet(new Set()) // For small collections
2184
- const largeMap = new ReactiveMap(new Map()) // For large collections with key access
2185
- ```
2186
-
2187
- ## Debugging and Development
2188
-
2189
- ### Debug Options
2190
-
2191
- Configure debug behavior:
2192
-
2193
- ```typescript
2194
- import { options as reactiveOptions } from 'mutts/reactive'
2195
-
2196
- // Track effect entry/exit
2197
- reactiveOptions.enter = (effect) => {
2198
- console.log('🔵 Entering effect:', effect.name || 'anonymous')
2199
- }
2200
-
2201
- reactiveOptions.leave = (effect) => {
2202
- console.log('🔴 Leaving effect:', effect.name || 'anonymous')
2203
- }
2204
-
2205
- // Track effect chaining
2206
- reactiveOptions.chain = (caller, target) => {
2207
- console.log('⛓️ Effect chain:', caller.name || 'anonymous', '->', target.name || 'anonymous')
2208
- }
2209
-
2210
- // Set maximum chain depth
2211
- reactiveOptions.maxEffectChain = 50
2212
-
2213
- // Set maximum deep watch traversal depth
2214
- reactiveOptions.maxDeepWatchDepth = 200
2215
- ```
2216
-
2217
- ### Effect Stack Traces
2218
-
2219
- Debug effect execution:
2220
-
2221
- ```typescript
2222
- const state = reactive({ count: 0 })
2223
-
2224
- effect(() => {
2225
- console.trace('Effect running')
2226
- console.log('Count:', state.count)
2227
- })
2228
-
2229
- // This will show the call stack when the effect runs
2230
- state.count = 5
2231
- ```
2232
-
2233
- ### Evolution Inspection
2234
-
2235
- Inspect object evolution history:
2236
-
2237
- ```typescript
2238
- const obj = reactive({ x: 1 })
2239
- let state = getState(obj)
2240
-
2241
- effect(() => {
2242
- console.log('=== Evolution History ===')
2243
- let depth = 0
2244
- while ('evolution' in state) {
2245
- console.log(`${' '.repeat(depth)}${state.evolution.type}: ${state.evolution.prop}`)
2246
- state = state.next
2247
- depth++
2248
- }
2249
- console.log('=== End History ===')
2250
-
2251
- // Reset state reference
2252
- state = getState(obj)
2253
- })
2254
-
2255
- obj.x = 2
2256
- obj.y = 'new'
2257
- delete obj.x
2258
- ```
2259
-
2260
- ## API Reference
2261
-
2262
- ### Decorators
2263
-
2264
- #### `@computed`
2265
-
2266
- Marks a class accessor as computed. The computed value will be cached and invalidated when dependencies change.
2267
-
2268
- ```typescript
2269
- class MyClass {
2270
- private _value = 0;
2271
-
2272
- @computed
2273
- get doubled() {
2274
- return this._value * 2;
2275
- }
2276
- }
2277
-
2278
- // Function usage
2279
- function myExpensiveCalculus() {
2280
-
2281
- }
2282
- ...
2283
- const result = computed(myExpensiveCalculus);
2284
- ```
2285
-
2286
- **Use Cases:**
2287
- - Caching expensive calculations
2288
- - Derived state that depends on reactive values
2289
- - Computed properties in classes
2290
- - Performance optimization for frequently accessed values
2291
-
2292
- **Notes:**
2293
- By how JS works, writing `computed(()=> ...)` will always be wrong, as the notation `()=> ...` internally is a `new Function(...)`.
2294
- So, even if the return value is cached, it will never be used.
2295
-
2296
- #### `@atomic`
2297
-
2298
- Marks a class method as atomic, batching all effects triggered within the method until it completes.
2299
-
2300
- ```typescript
2301
- class MyClass {
2302
- @atomic
2303
- updateMultiple() {
2304
- this.a = 1
2305
- this.b = 2
2306
- this.c = 3
2307
- // Effects are batched and run only once after this method completes
2308
- }
2309
-
2310
- @atomic
2311
- updateAndReturn() {
2312
- this.a = 10
2313
- this.b = 20
2314
- return { sum: this.a + this.b, product: this.a * this.b }
2315
- }
2316
- }
2317
-
2318
- // Function usage
2319
- const result = atomic(() => {
2320
- state.a = 1
2321
- state.b = 2
2322
- return state.a + state.b
2323
- })
2324
- ```
2325
-
2326
- **Use Cases:**
2327
- - Batching multiple related state changes
2328
- - Performance optimization for methods with multiple updates
2329
- - Ensuring consistent state in effects
2330
- - Reducing unnecessary effect executions
2331
- - Returning computed values from atomic operations
2332
-
2333
- **Notes:**
2334
- - Effects are deferred until the method/function completes
2335
- - Nested atomic methods are batched at the outermost level
2336
- - Works with both class methods and standalone functions
2337
- - Methods and functions can return values (primitives, objects, functions)
2338
-
2339
- #### `@unreactive`
2340
-
2341
- Marks a class property as non-reactive. The property change will not be tracked by the reactive system.
2342
-
2343
- Marks a class (and its descendants) as non-reactive.
2344
-
2345
- ```typescript
2346
- class MyClass {
2347
- @unreactive
2348
- private config = { theme: 'dark' };
2349
- }
2350
-
2351
- // Class decorator usage
2352
- @unreactive
2353
- class NonReactiveClass {
2354
- // All instances will be non-reactive
2355
- }
2356
-
2357
- // Function usage
2358
- const nonReactiveObj = unreactive({ config: { theme: 'dark' } });
2359
- ```
2360
-
2361
- **Use Cases:**
2362
- - Configuration objects that shouldn't trigger reactivity
2363
- - Static data that never changes
2364
- - Performance optimization for objects that don't need tracking
2365
- - Third-party objects that shouldn't be made reactive
2366
-
2367
- ### Core Functions
2368
-
2369
- #### `reactive<T>(target: T): T`
2370
-
2371
- Creates a reactive proxy of the target object. All property access and mutations will be tracked.
2372
-
2373
- **Use Cases:**
2374
- - Converting plain objects to reactive objects
2375
- - Making class instances reactive
2376
- - Creating reactive arrays, maps, and sets
2377
- - Setting up reactive state management
2378
-
2379
- ```typescript
2380
- const state = reactive({ count: 0, name: 'John' });
2381
- const items = reactive([1, 2, 3]);
2382
- const map = reactive(new Map([['key', 'value']]));
2383
- ```
2384
-
2385
- #### `effect(fn, ...args): ScopedCallback`
2386
-
2387
- Creates a reactive effect that runs when its dependencies change.
2388
-
2389
- **Use Cases:**
2390
- - Side effects like DOM updates
2391
- - Logging and debugging
2392
- - Data synchronization
2393
- - Cleanup operations
2394
-
2395
- ```typescript
2396
- const cleanup = effect((dep) => {
2397
- console.log('Count changed:', state.count);
2398
- return () => {
2399
- // Cleanup function
2400
- };
2401
- });
2402
- ```
2403
-
2404
- #### `atomic<T>(fn: () => T): T`
2405
-
2406
- Creates an atomic operation that batches all effects triggered within the function until it completes.
2407
-
2408
- **Use Cases:**
2409
- - Batching multiple related state changes
2410
- - Performance optimization for functions with multiple updates
2411
- - Ensuring consistent state in effects
2412
- - Reducing unnecessary effect executions
2413
-
2414
- ```typescript
2415
- const result = atomic(() => {
2416
- state.a = 1
2417
- state.b = 2
2418
- return state.a + state.b
2419
- })
2420
- ```
2421
-
2422
- #### `computed<T>(getter: ComputedFunction<T>): T`
2423
-
2424
- Creates a computed value that caches its result and recomputes when dependencies change.
2425
-
2426
- **Use Cases:**
2427
- - Derived state calculations
2428
- - Expensive computations
2429
- - Data transformations
2430
- - Conditional logic based on reactive state
2431
-
2432
- ```typescript
2433
- const result = computed(someExpensiveCalculus);
2434
- ```
2435
-
2436
- #### `watch(value, callback, options?): ScopedCallback`
2437
-
2438
- Watches a reactive value or function and calls a callback when it changes.
2439
-
2440
- **Use Cases:**
2441
- - Reacting to specific value changes
2442
- - Debugging reactive state
2443
- - Side effects for specific properties
2444
- - Data validation
2445
-
2446
- ```typescript
2447
- // Watch a specific value
2448
- const stop = watch(() => state.count, (newVal, oldVal) => {
2449
- console.log(`Count changed from ${oldVal} to ${newVal}`);
2450
- });
2451
-
2452
- // Watch an object with deep option
2453
- const stopDeep = watch(state, (newState) => {
2454
- console.log('State changed:', newState);
2455
- }, { deep: true, immediate: true });
2456
- ```
2457
-
2458
- #### `unwrap<T>(proxy: T): T`
2459
-
2460
- Returns the original object from a reactive proxy.
2461
-
2462
- **Use Cases:**
2463
- - Accessing original object for serialization
2464
- - Passing to non-reactive functions
2465
- - Performance optimization
2466
- - Debugging
2467
-
2468
- ```typescript
2469
- const original = unwrap(reactiveState);
2470
- JSON.stringify(original); // Safe to serialize
2471
- ```
2472
-
2473
- #### `isReactive(obj: any): boolean`
2474
-
2475
- Checks if an object is a reactive proxy.
2476
-
2477
- **Use Cases:**
2478
- - Type checking
2479
- - Debugging
2480
- - Conditional logic
2481
- - Validation
2482
-
2483
- ```typescript
2484
- if (isReactive(obj)) {
2485
- console.log('Object is reactive');
2486
- }
2487
- ```
2488
-
2489
- #### `isNonReactive(obj: any): boolean`
2490
-
2491
- Checks if an object is marked as non-reactive.
2492
-
2493
- **Use Cases:**
2494
- - Validation
2495
- - Debugging
2496
- - Conditional logic
2497
- - Type checking
2498
-
2499
- ```typescript
2500
- if (isNonReactive(obj)) {
2501
- console.log('Object is non-reactive');
2502
- }
2503
- ```
2504
-
2505
- #### `untracked(fn: () => void): void`
2506
-
2507
- Executes a function without tracking dependencies.
2508
-
2509
- **Use Cases:**
2510
- - Performance optimization
2511
- - Avoiding circular dependencies
2512
- - Side effects that shouldn't trigger reactivity
2513
- - Batch operations
2514
-
2515
- ```typescript
2516
- untracked(() => {
2517
- // This won't create dependencies
2518
- console.log('Untracked operation');
2519
- });
2520
- ```
2521
-
2522
- #### `getState(obj)`
2523
-
2524
- Gets the current state of a reactive object. Used internally for tracking changes.
2525
-
2526
- **Use Cases:**
2527
- - Debugging reactive state
2528
- - Custom reactive implementations
2529
- - State inspection
2530
-
2531
- #### `invalidateComputed(callback, warn?)`
2532
-
2533
- Registers a callback to be called when a computed property is invalidated.
2534
-
2535
- **Use Cases:**
2536
- - Custom computed implementations
2537
- - Cleanup operations
2538
- - Performance monitoring
2539
-
2540
- ```typescript
2541
- const computed = computed(() => {
2542
- invalidateComputed(() => {
2543
- console.log('Computed invalidated');
2544
- });
2545
- return expensiveCalculation();
2546
- });
2547
- ```
2548
-
2549
- ### Configuration
2550
-
2551
- #### `reactiveOptions`
2552
-
2553
- Global options for the reactive system.
2554
-
2555
- **Properties:**
2556
- - `enter(effect: Function)`: Called when an effect is entered
2557
- - `leave(effect: Function)`: Called when an effect is left
2558
- - `chain(target: Function, caller?: Function)`: Called when effects are chained
2559
- - `maxEffectChain: number`: Maximum effect chain depth (default: 100)
2560
- - `maxDeepWatchDepth: number`: Maximum deep watch traversal depth (default: 100)
2561
- - `instanceMembers: boolean`: Only react on instance members (default: true)
2562
- - `warn(...args: any[])`: Warning function (default: console.warn)
2563
-
2564
- **Use Cases:**
2565
- - Debugging reactive behavior
2566
- - Performance tuning
2567
- - Custom logging
2568
- - Error handling
2569
-
2570
- ```typescript
2571
- reactiveOptions.maxEffectChain = 50;
2572
- reactiveOptions.enter = (effect) => console.log('Effect entered:', effect.name);
2573
- ```
2574
-
2575
- ### Classes
2576
-
2577
- #### `ReactiveBase`
2578
-
2579
- Base class for reactive objects. When extended, instances are automatically made reactive.
2580
-
2581
- **Use Cases:**
2582
- - Creating reactive classes
2583
- - Automatic reactivity for class instances
2584
- - Type safety for reactive objects
2585
-
2586
- ```typescript
2587
- class MyState extends ReactiveBase {
2588
- count = 0;
2589
- name = '';
2590
- }
2591
-
2592
- const state = new MyState(); // Automatically reactive
2593
- ```
2594
-
2595
- #### `ReactiveError`
2596
-
2597
- Error class for reactive system errors.
2598
-
2599
- **Use Cases:**
2600
- - Error handling in reactive code
2601
- - Debugging reactive issues
2602
- - Custom error types
2603
-
2604
- ### Collections
2605
-
2606
- Collections (Array, Map, Set, WeakMap, WeakSet) can be automatically made reactive when passed to `reactive()`:
2607
-
2608
- ```typescript
2609
- const items = reactive([1, 2, 3]);
2610
- items.push(4); // Triggers reactivity
2611
-
2612
- const map = reactive(new Map([['key', 'value']]));
2613
- map.set('newKey', 'newValue'); // Triggers reactivity
2614
-
2615
- const set = reactive(new Set([1, 2, 3]));
2616
- set.add(4); // Triggers reactivity
2617
- ```
2618
-
2619
- #### Automatic Collection Reactivity
2620
-
2621
- All native collections have their specific management:
2622
-
2623
- ```typescript
2624
- // Collections still need to be wrapped with reactive()
2625
- const arr = reactive([1, 2, 3]) // ReactiveArray
2626
- const map = reactive(new Map()) // ReactiveMap
2627
- const set = reactive(new Set()) // ReactiveSet
2628
- const weakMap = reactive(new WeakMap()) // ReactiveWeakMap
2629
- const weakSet = reactive(new WeakSet()) // ReactiveWeakSet
2630
-
2631
- effect(() => {
2632
- console.log('Array length:', arr.length)
2633
- console.log('Map size:', map.size)
2634
- console.log('Set size:', set.size)
2635
- })
2636
-
2637
- arr.push(4) // Triggers effect
2638
- map.set('key', 'value') // Triggers effect
2639
- set.add('item') // Triggers effect
2640
- ```
2641
-
2642
- **Use Cases:**
2643
- - Applications that primarily work with reactive collections
2644
- - Global reactive state management
2645
- - Ensuring collection methods (push, set, add, etc.) trigger reactivity
2646
- - Performance optimization for collection-heavy applications
2647
-
2648
- **Note:** This module registers native collection types to use specialized reactive wrappers. Without importing this module, collections wrapped with `reactive()` will only have basic object reactivity - collection methods like `map.set()`, `array.push()`, etc. will not trigger effects. The `reactive()` wrapper is still required, but the collections module ensures proper reactive behavior for collection-specific operations.
2649
-
2650
- ### Types
2651
-
2652
- #### `ScopedCallback`
2653
-
2654
- Type for effect cleanup functions.
2655
-
2656
- #### `DependencyFunction`
2657
-
2658
- Type for dependency tracking functions used in effects and computed values.
2659
-
2660
- #### `WatchOptions`
2661
-
2662
- Options for the watch function:
2663
- - `immediate?: boolean`: Call callback immediately
2664
- - `deep?: boolean`: Watch nested properties
2665
-
2666
- ### Profile Information
2667
-
2668
- #### `profileInfo`
2669
-
2670
- Object containing internal reactive system state for debugging and profiling.
2671
-
2672
- **Properties:**
2673
- - `objectToProxy`: WeakMap of original objects to their proxies
2674
- - `proxyToObject`: WeakMap of proxies to their original objects
2675
- - `effectToReactiveObjects`: WeakMap of effects to watched objects
2676
- - `watchers`: WeakMap of objects to their property watchers
2677
- - `objectParents`: WeakMap of objects to their parent relationships
2678
- - `objectsWithDeepWatchers`: WeakSet of objects with deep watchers
2679
- - `deepWatchers`: WeakMap of objects to their deep watchers
2680
- - `effectToDeepWatchedObjects`: WeakMap of effects to deep watched objects
2681
- - `nonReactiveObjects`: WeakSet of non-reactive objects
2682
- - `computedCache`: WeakMap of computed functions to their cached values
2683
-
2684
- **Use Cases:**
2685
- - Debugging reactive behavior
2686
- - Performance profiling
2687
- - Memory leak detection
2688
- - System state inspection
1
+ # Reactive System Documentation
2
+
3
+ The Mutts Reactive System documentation has been split into focused sections for better readability.
4
+
5
+ ## [Core Concepts](./reactive/core.md)
6
+ * **[Core API](./reactive/core.md#core-api)**: `reactive`, `effect`, `unwrap`
7
+ * **[Effect System](./reactive/core.md#effect-system)**: Dependency tracking, cleanups, async effects
8
+ * **[Class Reactivity](./reactive/core.md#class-reactivity)**: Decorators and functional syntax
9
+
10
+ ## [Collections](./reactive/collections.md)
11
+ * **[Reactive Collections](./reactive/collections.md#collections)**: Map, Set, WeakMap, WeakSet
12
+ * **[Reactive Arrays](./reactive/collections.md#reactivearray)**: Full array method support
13
+ * **[Register](./reactive/collections.md#register)**: ID-keyed ordered collections
14
+ * **[Projections](./reactive/collections.md#projection)**: `project`, `mapped`, `organized`
15
+
16
+ ## [Advanced Topics](./reactive/advanced.md)
17
+ * **[Atomic Operations](./reactive/advanced.md#atomic-operations)**: Batching and Bidirectional binding
18
+ * **[Evolution Tracking](./reactive/advanced.md#evolution-tracking)**: History introspection
19
+ * **[Prototype Chains](./reactive/advanced.md#prototype-chains-and-pure-objects)**: Advanced inheritance patterns
20
+ * **[Memoization](./reactive/advanced.md#memoization)**: Caching strategies
21
+ * **[Debugging](./reactive/advanced.md#debugging-and-development)**: Cycle detection and troubleshooting