mutts 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -0
- package/dist/chunks/decorator-BXsign4Z.js +176 -0
- package/dist/chunks/decorator-BXsign4Z.js.map +1 -0
- package/dist/chunks/decorator-CPbZNnsX.esm.js +168 -0
- package/dist/chunks/decorator-CPbZNnsX.esm.js.map +1 -0
- package/dist/decorator.d.ts +50 -0
- package/dist/decorator.esm.js +2 -0
- package/dist/decorator.esm.js.map +1 -0
- package/dist/decorator.js +11 -0
- package/dist/decorator.js.map +1 -0
- package/dist/destroyable.d.ts +48 -0
- package/dist/destroyable.esm.js +91 -0
- package/dist/destroyable.esm.js.map +1 -0
- package/dist/destroyable.js +98 -0
- package/dist/destroyable.js.map +1 -0
- package/dist/eventful.d.ts +11 -0
- package/dist/eventful.esm.js +88 -0
- package/dist/eventful.esm.js.map +1 -0
- package/dist/eventful.js +90 -0
- package/dist/eventful.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.esm.js +7 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/indexable.d.ts +31 -0
- package/dist/indexable.esm.js +85 -0
- package/dist/indexable.esm.js.map +1 -0
- package/dist/indexable.js +89 -0
- package/dist/indexable.js.map +1 -0
- package/dist/mutts.umd.js +2 -0
- package/dist/mutts.umd.js.map +1 -0
- package/dist/mutts.umd.min.js +2 -0
- package/dist/mutts.umd.min.js.map +1 -0
- package/dist/promiseChain.d.ts +11 -0
- package/dist/promiseChain.esm.js +72 -0
- package/dist/promiseChain.esm.js.map +1 -0
- package/dist/promiseChain.js +74 -0
- package/dist/promiseChain.js.map +1 -0
- package/dist/reactive.d.ts +114 -0
- package/dist/reactive.esm.js +1455 -0
- package/dist/reactive.esm.js.map +1 -0
- package/dist/reactive.js +1472 -0
- package/dist/reactive.js.map +1 -0
- package/dist/std-decorators.d.ts +17 -0
- package/dist/std-decorators.esm.js +161 -0
- package/dist/std-decorators.esm.js.map +1 -0
- package/dist/std-decorators.js +169 -0
- package/dist/std-decorators.js.map +1 -0
- package/docs/decorator.md +300 -0
- package/docs/destroyable.md +294 -0
- package/docs/events.md +225 -0
- package/docs/indexable.md +561 -0
- package/docs/promiseChain.md +218 -0
- package/docs/reactive.md +2072 -0
- package/docs/std-decorators.md +558 -0
- package/package.json +132 -0
- package/src/decorator.test.ts +495 -0
- package/src/decorator.ts +205 -0
- package/src/destroyable.test.ts +155 -0
- package/src/destroyable.ts +158 -0
- package/src/eventful.test.ts +380 -0
- package/src/eventful.ts +69 -0
- package/src/index.ts +7 -0
- package/src/indexable.test.ts +388 -0
- package/src/indexable.ts +124 -0
- package/src/promiseChain.test.ts +201 -0
- package/src/promiseChain.ts +99 -0
- package/src/reactive/array.test.ts +923 -0
- package/src/reactive/array.ts +352 -0
- package/src/reactive/core.test.ts +1663 -0
- package/src/reactive/core.ts +866 -0
- package/src/reactive/index.ts +28 -0
- package/src/reactive/interface.test.ts +1477 -0
- package/src/reactive/interface.ts +231 -0
- package/src/reactive/map.test.ts +866 -0
- package/src/reactive/map.ts +162 -0
- package/src/reactive/set.test.ts +289 -0
- package/src/reactive/set.ts +142 -0
- package/src/std-decorators.test.ts +679 -0
- package/src/std-decorators.ts +182 -0
- package/src/utils.ts +52 -0
package/docs/reactive.md
ADDED
|
@@ -0,0 +1,2072 @@
|
|
|
1
|
+
# Reactive Documentation
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## Introduction
|
|
5
|
+
|
|
6
|
+
### What is Reactivity?
|
|
7
|
+
|
|
8
|
+
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.
|
|
9
|
+
|
|
10
|
+
### Core Concepts
|
|
11
|
+
|
|
12
|
+
- **Reactive Objects**: Plain JavaScript objects wrapped with reactive capabilities
|
|
13
|
+
- **Effects**: Functions that automatically re-run when their dependencies change
|
|
14
|
+
- **Dependencies**: Reactive properties that an effect depends on
|
|
15
|
+
- **Evolution Tracking**: Built-in change history for reactive objects
|
|
16
|
+
- **Collections**: Reactive wrappers for Array, Map, Set, WeakMap, and WeakSet
|
|
17
|
+
|
|
18
|
+
### Basic Example
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { reactive, effect } from 'mutts/reactive'
|
|
22
|
+
|
|
23
|
+
// Create a reactive object
|
|
24
|
+
const user = reactive({ name: "John", age: 30 })
|
|
25
|
+
|
|
26
|
+
// Create an effect that depends on user properties
|
|
27
|
+
effect(() => {
|
|
28
|
+
console.log(`User: ${user.name}, Age: ${user.age}`)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// When properties change, the effect automatically re-runs
|
|
32
|
+
user.name = "Jane" // Triggers effect
|
|
33
|
+
user.age = 25 // Triggers effect
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Getting Started
|
|
37
|
+
|
|
38
|
+
### Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install mutts
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Basic Usage
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { reactive, effect } from 'mutts/reactive'
|
|
48
|
+
|
|
49
|
+
// Make an object reactive
|
|
50
|
+
const state = reactive({
|
|
51
|
+
count: 0,
|
|
52
|
+
message: "Hello"
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Create reactive effects
|
|
56
|
+
effect(() => {
|
|
57
|
+
console.log(`Count: ${state.count}`)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
effect(() => {
|
|
61
|
+
console.log(`Message: ${state.message}`)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// Changes trigger effects automatically
|
|
65
|
+
state.count++ // Triggers first effect
|
|
66
|
+
state.message = "Hi" // Triggers second effect
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Hello World Example
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { reactive, effect } from 'mutts/reactive'
|
|
73
|
+
|
|
74
|
+
// Simple counter
|
|
75
|
+
const counter = reactive({ value: 0 })
|
|
76
|
+
|
|
77
|
+
effect(() => {
|
|
78
|
+
document.body.innerHTML = `Count: ${counter.value}`
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// Button click handler
|
|
82
|
+
document.getElementById('increment').onclick = () => {
|
|
83
|
+
counter.value++
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Core API
|
|
88
|
+
|
|
89
|
+
### `reactive()`
|
|
90
|
+
|
|
91
|
+
Makes an object reactive by wrapping it in a proxy.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
function reactive<T extends Record<PropertyKey, any>>(target: T): T
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Parameters:**
|
|
98
|
+
- `target`: The object to make reactive
|
|
99
|
+
|
|
100
|
+
**Returns:** A reactive proxy of the original object
|
|
101
|
+
|
|
102
|
+
**Example:**
|
|
103
|
+
```typescript
|
|
104
|
+
const obj = { count: 0 }
|
|
105
|
+
const reactiveObj = reactive(obj)
|
|
106
|
+
|
|
107
|
+
// reactiveObj is now reactive
|
|
108
|
+
effect(() => {
|
|
109
|
+
console.log(reactiveObj.count) // Tracks dependency
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
reactiveObj.count = 5 // Triggers effect
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Note:** The same object will always return the same proxy instance.
|
|
116
|
+
|
|
117
|
+
### `effect()`
|
|
118
|
+
|
|
119
|
+
Creates a reactive effect that automatically re-runs when dependencies change.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
function effect(
|
|
123
|
+
fn: (dep: DependencyFunction, ...args: any[]) => (ScopedCallback | undefined | void),
|
|
124
|
+
...args: any[]
|
|
125
|
+
): ScopedCallback
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Parameters:**
|
|
129
|
+
- `fn`: The effect function that provides dependencies and may return a cleanup function
|
|
130
|
+
- `...args`: Additional arguments that are forwarded to the effect function
|
|
131
|
+
|
|
132
|
+
**Returns:** A cleanup function to stop the effect
|
|
133
|
+
|
|
134
|
+
**Example:**
|
|
135
|
+
```typescript
|
|
136
|
+
const state = reactive({ count: 0, mood: 'happy' })
|
|
137
|
+
|
|
138
|
+
const cleanup = effect(() => {
|
|
139
|
+
console.log(`Count is: ${state.count}`)
|
|
140
|
+
// Optional cleanup called before next run
|
|
141
|
+
return () => console.log('Cleaning up...')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
state.count++ // Does trigger: 1- the cleaning, 2- the effect
|
|
145
|
+
state.mood = 'surprised' // Does not trigger the effect
|
|
146
|
+
|
|
147
|
+
// Later...
|
|
148
|
+
cleanup() // Stops the effect
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Using effect with arguments (useful in loops):**
|
|
152
|
+
```typescript
|
|
153
|
+
const items = reactive([{ id: 1 }, { id: 2 }, { id: 3 }])
|
|
154
|
+
|
|
155
|
+
// Create effects in a loop, passing loop variables
|
|
156
|
+
for (let i = 0; i < items.length; i++) {
|
|
157
|
+
effect((dep, index) => {
|
|
158
|
+
console.log(`Item ${index}:`, items[index])
|
|
159
|
+
}, i) // Pass the loop variable as argument
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### `unwrap()`
|
|
164
|
+
|
|
165
|
+
Gets the original, non-reactive object from a reactive proxy.
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
function unwrap<T>(proxy: T): T
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Example:**
|
|
172
|
+
```typescript
|
|
173
|
+
const original = { count: 0 }
|
|
174
|
+
const reactive = reactive(original)
|
|
175
|
+
const unwrapped = unwrap(reactive)
|
|
176
|
+
|
|
177
|
+
console.log(unwrapped === original) // true
|
|
178
|
+
console.log(unwrapped === reactive) // false
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### `isReactive()`
|
|
182
|
+
|
|
183
|
+
Checks if an object is a reactive proxy.
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
function isReactive(obj: any): boolean
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### `isNonReactive()`
|
|
190
|
+
|
|
191
|
+
Checks if an object is marked as non-reactive.
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
function isNonReactive(obj: any): boolean
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Effect System
|
|
198
|
+
|
|
199
|
+
### Basic Effects
|
|
200
|
+
|
|
201
|
+
Effects are the core of the reactive system. They automatically track dependencies and re-run when those dependencies change.
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
const state = reactive({ count: 0, name: "John" })
|
|
205
|
+
|
|
206
|
+
effect(() => {
|
|
207
|
+
// This effect depends on state.count
|
|
208
|
+
console.log(`Count: ${state.count}`)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// Only changing count triggers the effect
|
|
212
|
+
state.count = 5 // Triggers effect
|
|
213
|
+
state.name = "Jane" // Does NOT trigger effect
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Effect Cleanup
|
|
217
|
+
|
|
218
|
+
Effects return cleanup functions that you can call to stop tracking dependencies.
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
const state = reactive({ count: 0 })
|
|
222
|
+
|
|
223
|
+
const stopEffect = effect(() => {
|
|
224
|
+
console.log(`Count: ${state.count}`)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
// Later...
|
|
228
|
+
stopEffect() // Stops the effect
|
|
229
|
+
|
|
230
|
+
state.count = 10 // No longer triggers the effect
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Automatic Effect Cleanup
|
|
234
|
+
|
|
235
|
+
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.
|
|
236
|
+
|
|
237
|
+
#### How Automatic Cleanup Works
|
|
238
|
+
|
|
239
|
+
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.
|
|
240
|
+
|
|
241
|
+
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.
|
|
242
|
+
|
|
243
|
+
#### Examples
|
|
244
|
+
|
|
245
|
+
**Parent-Child Independence:**
|
|
246
|
+
```typescript
|
|
247
|
+
const state = reactive({ a: 1, b: 2 })
|
|
248
|
+
|
|
249
|
+
const stopParent = effect(() => {
|
|
250
|
+
state.a
|
|
251
|
+
|
|
252
|
+
// Child effect - becomes independent when parent is cleaned up
|
|
253
|
+
effect(() => {
|
|
254
|
+
state.b
|
|
255
|
+
return () => console.log('Child cleanup')
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
return () => console.log('Parent cleanup')
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// Only cleans up the parent - child becomes independent
|
|
262
|
+
stopParent() // Logs: "Parent cleanup" (child continues running)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Garbage Collection Cleanup:**
|
|
266
|
+
```typescript
|
|
267
|
+
const state = reactive({ value: 1 })
|
|
268
|
+
|
|
269
|
+
// Top-level effect - automatically cleaned up via garbage collection
|
|
270
|
+
effect(() => {
|
|
271
|
+
state.value
|
|
272
|
+
return () => console.log('GC cleanup')
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
// No explicit cleanup needed - will be cleaned up when garbage collected
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**When You Should Store Cleanup Functions:**
|
|
279
|
+
|
|
280
|
+
While cleanup is automatic, you should **store and remember** cleanup functions when your effects have side effects that need immediate cleanup:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
const state = reactive({ value: 1 })
|
|
284
|
+
const activeEffects: (() => void)[] = []
|
|
285
|
+
|
|
286
|
+
// Store cleanup functions for effects with side effects
|
|
287
|
+
for (let i = 0; i < 3; i++) {
|
|
288
|
+
const stopEffect = effect(() => {
|
|
289
|
+
state.value
|
|
290
|
+
|
|
291
|
+
// Side effect that needs immediate cleanup
|
|
292
|
+
const intervalId = setInterval(() => {
|
|
293
|
+
console.log(`Timer ${i} running`)
|
|
294
|
+
}, 1000)
|
|
295
|
+
|
|
296
|
+
return () => {
|
|
297
|
+
clearInterval(intervalId) // Prevent memory leaks
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
activeEffects.push(stopEffect)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Clean up all effects when needed
|
|
305
|
+
activeEffects.forEach(stop => stop())
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Key Points:**
|
|
309
|
+
|
|
310
|
+
- **You do not have to call cleanup** - the system handles it automatically via garbage collection
|
|
311
|
+
- **You may want to call cleanup** - especially for effects with side effects
|
|
312
|
+
- **You have to store and remember cleanup** - when you need immediate control over when effects stop
|
|
313
|
+
- **Child effects become independent** - when their parent effect is cleaned up, they continue running
|
|
314
|
+
- **All effects use garbage collection** - as the primary cleanup mechanism
|
|
315
|
+
|
|
316
|
+
### Effect Dependencies
|
|
317
|
+
|
|
318
|
+
Effects automatically track which reactive properties they access.
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
const state = reactive({ a: 1, b: 2, c: 3 })
|
|
322
|
+
|
|
323
|
+
effect(() => {
|
|
324
|
+
// Only tracks state.a and state.b
|
|
325
|
+
console.log(`Sum: ${state.a + state.b}`)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
// Only these changes trigger the effect
|
|
329
|
+
state.a = 5 // Triggers effect
|
|
330
|
+
state.b = 10 // Triggers effect
|
|
331
|
+
state.c = 15 // Does NOT trigger effect
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Async Effects and the `dep` Parameter
|
|
335
|
+
|
|
336
|
+
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.
|
|
337
|
+
|
|
338
|
+
#### The Problem with Async Effects
|
|
339
|
+
|
|
340
|
+
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.
|
|
341
|
+
|
|
342
|
+
The `dep` parameter restores the active effect context for dependency tracking.
|
|
343
|
+
|
|
344
|
+
#### Understanding the active effect context
|
|
345
|
+
|
|
346
|
+
The reactive system uses a global active effect variable to track which effect is currently running:
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
// Synchronous effect - active effect is maintained throughout
|
|
350
|
+
effect(() => {
|
|
351
|
+
// active effect = this effect
|
|
352
|
+
const value = state.count // ✅ Tracked (active effect is set)
|
|
353
|
+
// active effect = this effect (still set)
|
|
354
|
+
const another = state.name // ✅ Tracked (active effect is still set)
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
// Async effect - active effect is lost after await
|
|
358
|
+
effect(async () => {
|
|
359
|
+
// active effect = this effect
|
|
360
|
+
const value = state.count // ✅ Tracked (active effect is set)
|
|
361
|
+
|
|
362
|
+
await someAsyncOperation() // Function exits, active effect = undefined
|
|
363
|
+
|
|
364
|
+
// active effect = undefined (lost!)
|
|
365
|
+
const another = state.name // ❌ NOT tracked (no active effect)
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
// Async effect with dep() - active effect is restored
|
|
369
|
+
effect(async (dep) => {
|
|
370
|
+
// active effect = this effect
|
|
371
|
+
const value = state.count // ✅ Tracked (active effect is set)
|
|
372
|
+
|
|
373
|
+
await someAsyncOperation() // Function exits, active effect = undefined
|
|
374
|
+
|
|
375
|
+
// dep() temporarily restores active effect for the callback
|
|
376
|
+
const another = dep(() => state.name) // ✅ Tracked (active effect restored)
|
|
377
|
+
})
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
#### Key Benefits of `dep()` in Async Effects
|
|
381
|
+
|
|
382
|
+
1. **Restored Context**: `dep()` temporarily restores the active effect context for dependency tracking
|
|
383
|
+
2. **Consistent Tracking**: Reactive property access works the same way before and after `await`
|
|
384
|
+
|
|
385
|
+
### Nested Effects
|
|
386
|
+
|
|
387
|
+
Effects can be created inside other effects and will have separate effect scopes:
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
import { effect, reactive } from 'mutts/reactive'
|
|
391
|
+
|
|
392
|
+
const state = reactive({ a: 0, b: 0 })
|
|
393
|
+
|
|
394
|
+
const stopOuter = effect(() => {
|
|
395
|
+
state.a
|
|
396
|
+
|
|
397
|
+
// Create an inner effect with its own scope
|
|
398
|
+
const stopInner = effect(() => {
|
|
399
|
+
state.b
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
// Return cleanup function for the inner effect
|
|
403
|
+
return stopInner
|
|
404
|
+
})
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### `untracked()`
|
|
408
|
+
|
|
409
|
+
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.
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
import { effect, untracked, reactive } from 'mutts/reactive'
|
|
413
|
+
|
|
414
|
+
const state = reactive({ a: 0, b: 0 })
|
|
415
|
+
|
|
416
|
+
effect(() => {
|
|
417
|
+
state.a
|
|
418
|
+
|
|
419
|
+
// Create an inner effect without tracking the creation under the outer effect
|
|
420
|
+
let stopInner: (() => void) | undefined
|
|
421
|
+
untracked(() => {
|
|
422
|
+
stopInner = effect(() => {
|
|
423
|
+
state.b
|
|
424
|
+
})
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
// Optionally stop it immediately to avoid accumulating watchers
|
|
428
|
+
stopInner && stopInner()
|
|
429
|
+
})
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**Use cases for `untracked()`:**
|
|
433
|
+
- Creating effects inside other effects without coupling their dependencies
|
|
434
|
+
- Performing side effects that shouldn't trigger when dependencies change
|
|
435
|
+
- Avoiding circular dependencies in complex reactive systems
|
|
436
|
+
|
|
437
|
+
### Effect Options and debugging
|
|
438
|
+
|
|
439
|
+
Configure the reactive system behavior:
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
import { options as reactiveOptions } from 'mutts/reactive'
|
|
443
|
+
|
|
444
|
+
// Set maximum effect chain depth
|
|
445
|
+
reactiveOptions.maxEffectChain = 50
|
|
446
|
+
|
|
447
|
+
// Set maximum deep watch traversal depth
|
|
448
|
+
reactiveOptions.maxDeepWatchDepth = 200
|
|
449
|
+
|
|
450
|
+
// Enable debug logging
|
|
451
|
+
reactiveOptions.enter = (effect) => console.log('Entering effect:', effect)
|
|
452
|
+
reactiveOptions.leave = (effect) => console.log('Leaving effect:', effect)
|
|
453
|
+
reactiveOptions.chain = (caller, target) => console.log('Chaining:', caller, '->', target)
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
## Advanced Effects
|
|
457
|
+
|
|
458
|
+
### Recording Results inside Effects
|
|
459
|
+
|
|
460
|
+
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.
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
const state = reactive({ count: 0 })
|
|
464
|
+
const results: number[] = []
|
|
465
|
+
|
|
466
|
+
const stop = effect(() => {
|
|
467
|
+
results.push(state.count * 2)
|
|
468
|
+
return () => {
|
|
469
|
+
// cleanup between runs if needed
|
|
470
|
+
}
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
state.count = 5 // results: [0, 10]
|
|
474
|
+
stop()
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Effect Cleanup Functions
|
|
478
|
+
|
|
479
|
+
Effects can return cleanup functions that run before the next execution.
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
const state = reactive({ count: 0 })
|
|
483
|
+
|
|
484
|
+
effect(() => {
|
|
485
|
+
const interval = setInterval(() => {
|
|
486
|
+
console.log('Count:', state.count)
|
|
487
|
+
}, 1000)
|
|
488
|
+
|
|
489
|
+
return () => {
|
|
490
|
+
clearInterval(interval)
|
|
491
|
+
console.log('Cleaned up interval')
|
|
492
|
+
}
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
// When state.count changes, the cleanup runs first
|
|
496
|
+
state.count = 5
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### Effect Lifecycle
|
|
500
|
+
|
|
501
|
+
1. **Initial Run**: Effect runs immediately when created
|
|
502
|
+
2. **Dependency Change**: When dependencies change, cleanup runs, then effect re-runs
|
|
503
|
+
3. **Manual Stop**: Calling the cleanup function stops the effect permanently
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
const state = reactive({ count: 0 })
|
|
507
|
+
|
|
508
|
+
const stop = effect(() => {
|
|
509
|
+
console.log('Effect running, count:', state.count)
|
|
510
|
+
return () => console.log('Cleanup running')
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
console.log('Effect created')
|
|
514
|
+
|
|
515
|
+
state.count = 5
|
|
516
|
+
// Output:
|
|
517
|
+
// Effect created
|
|
518
|
+
// Effect running, count: 0
|
|
519
|
+
// Cleanup running
|
|
520
|
+
// Effect running, count: 5
|
|
521
|
+
|
|
522
|
+
stop() // Effect stops permanently
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Effect Arguments
|
|
526
|
+
|
|
527
|
+
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:
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
const items = reactive([
|
|
531
|
+
{ id: 1, name: 'Item 1' },
|
|
532
|
+
{ id: 2, name: 'Item 2' },
|
|
533
|
+
{ id: 3, name: 'Item 3' }
|
|
534
|
+
])
|
|
535
|
+
|
|
536
|
+
// Create effects for each item with the item index
|
|
537
|
+
const effectCleanups: (() => void)[] = []
|
|
538
|
+
|
|
539
|
+
for (let i = 0; i < items.length; i++) {
|
|
540
|
+
const cleanup = effect((dep, index) => {
|
|
541
|
+
console.log(`Item ${index}:`, items[index].name)
|
|
542
|
+
|
|
543
|
+
// The index is captured in the closure and passed as argument
|
|
544
|
+
return () => console.log(`Cleaning up effect for item ${index}`)
|
|
545
|
+
}, i)
|
|
546
|
+
|
|
547
|
+
effectCleanups.push(cleanup)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Later, clean up all effects
|
|
551
|
+
effectCleanups.forEach(cleanup => cleanup())
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### Watch Function
|
|
555
|
+
|
|
556
|
+
The `watch` function provides a more direct way to observe changes in reactive objects. It comes in two forms:
|
|
557
|
+
|
|
558
|
+
#### Watch with Value Function
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
const state = reactive({ count: 0, name: 'John' })
|
|
562
|
+
|
|
563
|
+
const stop = watch(
|
|
564
|
+
() => state.count, // Value function
|
|
565
|
+
(newValue, oldValue) => {
|
|
566
|
+
console.log(`Count changed from ${oldValue} to ${newValue}`)
|
|
567
|
+
}
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
state.count = 5 // Triggers: "Count changed from 0 to 5"
|
|
571
|
+
state.name = 'Jane' // No trigger (not watching name)
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
#### Watch Object Properties
|
|
575
|
+
|
|
576
|
+
The second form of `watch` allows you to watch any property change on a reactive object:
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
const user = reactive({
|
|
580
|
+
name: 'John',
|
|
581
|
+
age: 30,
|
|
582
|
+
email: 'john@example.com'
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
const stop = watch(
|
|
586
|
+
user, // The reactive object to watch
|
|
587
|
+
() => {
|
|
588
|
+
console.log('Any property of user changed!')
|
|
589
|
+
console.log('Current user:', user)
|
|
590
|
+
}
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
user.name = 'Jane' // Triggers the callback
|
|
594
|
+
user.age = 31 // Triggers the callback
|
|
595
|
+
user.email = 'jane@example.com' // Triggers the callback
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
#### Use Cases
|
|
599
|
+
|
|
600
|
+
**Object-level watching** is particularly useful for:
|
|
601
|
+
|
|
602
|
+
- **Form validation**: Watch all form fields for changes
|
|
603
|
+
- **Auto-save**: Save whenever any field in a document changes
|
|
604
|
+
- **Logging**: Track all changes to a state object
|
|
605
|
+
- **Dirty checking**: Detect if any property has been modified
|
|
606
|
+
|
|
607
|
+
```typescript
|
|
608
|
+
const form = reactive({
|
|
609
|
+
firstName: '',
|
|
610
|
+
lastName: '',
|
|
611
|
+
email: '',
|
|
612
|
+
isValid: false
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
const stop = watch(form, () => {
|
|
616
|
+
// Auto-save whenever any field changes
|
|
617
|
+
saveForm(form)
|
|
618
|
+
|
|
619
|
+
// Update validation status
|
|
620
|
+
form.isValid = form.firstName && form.lastName && form.email
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
// Any change to firstName, lastName, or email will trigger auto-save
|
|
624
|
+
form.firstName = 'John'
|
|
625
|
+
form.lastName = 'Doe'
|
|
626
|
+
form.email = 'john.doe@example.com'
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
#### Deep Watching
|
|
630
|
+
|
|
631
|
+
For both forms of `watch`, you can enable deep watching by passing `{ deep: true }` in the options:
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
const state = reactive({
|
|
635
|
+
user: {
|
|
636
|
+
name: 'John',
|
|
637
|
+
profile: { age: 30 }
|
|
638
|
+
}
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
// Deep watch - triggers on nested property changes
|
|
642
|
+
const stop = watch(state, () => {
|
|
643
|
+
console.log('Any nested property changed!')
|
|
644
|
+
}, { deep: true })
|
|
645
|
+
|
|
646
|
+
state.user.name = 'Jane' // Triggers
|
|
647
|
+
state.user.profile.age = 31 // Triggers (nested change)
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
**Deep Watching Behavior:**
|
|
651
|
+
|
|
652
|
+
- **Unreactive objects are skipped**: Deep watching will not traverse into objects marked as unreactive using `@unreactive` or `unreactive()`
|
|
653
|
+
- **Collections are handled specially**:
|
|
654
|
+
- **Arrays**: All elements and length changes are tracked
|
|
655
|
+
- **Sets**: All values are tracked (keys are not separate in Sets)
|
|
656
|
+
- **Maps**: All values are tracked (keys are not tracked separately)
|
|
657
|
+
- **WeakSet/WeakMap**: Cannot be deep watched (not iterable), only replacement triggers
|
|
658
|
+
- **Circular references**: Handled safely - There is also a configurable depth limit
|
|
659
|
+
- **Performance**: Deep watching has higher overhead than shallow watching
|
|
660
|
+
|
|
661
|
+
```typescript
|
|
662
|
+
// Example with collections
|
|
663
|
+
const state = reactive({
|
|
664
|
+
items: [1, 2, 3],
|
|
665
|
+
tags: new Set(['a', 'b']),
|
|
666
|
+
metadata: new Map([['key1', 'value1']])
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
const stop = watch(state, () => {
|
|
670
|
+
console.log('Collection changed')
|
|
671
|
+
}, { deep: true })
|
|
672
|
+
|
|
673
|
+
state.items.push(4) // Triggers
|
|
674
|
+
state.items[0] = 10 // Triggers
|
|
675
|
+
state.tags.add('c') // Triggers
|
|
676
|
+
state.metadata.set('key2', 'value2') // Triggers
|
|
677
|
+
|
|
678
|
+
// WeakSet/WeakMap only trigger on replacement
|
|
679
|
+
const weakSet = new WeakSet()
|
|
680
|
+
state.weakData = weakSet // Triggers (replacement)
|
|
681
|
+
// Changes to weakSet contents won't trigger (not trackable)
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
#### Cleanup
|
|
685
|
+
|
|
686
|
+
Both forms of `watch` return a cleanup function:
|
|
687
|
+
|
|
688
|
+
```typescript
|
|
689
|
+
const stop = watch(user, () => {
|
|
690
|
+
console.log('User changed')
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
// Later, stop watching
|
|
694
|
+
stop()
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
## Evolution Tracking
|
|
698
|
+
|
|
699
|
+
### Understanding Object Evolution
|
|
700
|
+
|
|
701
|
+
The reactive system tracks how objects change over time, creating an "evolution history" that you can inspect.
|
|
702
|
+
|
|
703
|
+
```typescript
|
|
704
|
+
import { getState } from './reactive'
|
|
705
|
+
|
|
706
|
+
const obj = reactive({ count: 0 })
|
|
707
|
+
let state = getState(obj)
|
|
708
|
+
|
|
709
|
+
effect(() => {
|
|
710
|
+
while ('evolution' in state) {
|
|
711
|
+
console.log('Change:', state.evolution)
|
|
712
|
+
state = state.next
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
console.log('Current count:', obj.count)
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
obj.count = 5
|
|
719
|
+
obj.count = 10
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
### `getState()` - Accessing Change History
|
|
723
|
+
|
|
724
|
+
Returns the current state object for tracking evolution.
|
|
725
|
+
|
|
726
|
+
```typescript
|
|
727
|
+
function getState(obj: any): State
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
**Returns:** A state object that does contain evolution information ( = `{}`) but will in a next evolution call if the object state has evolved.
|
|
731
|
+
|
|
732
|
+
### Evolution Types
|
|
733
|
+
|
|
734
|
+
The system tracks different types of changes:
|
|
735
|
+
|
|
736
|
+
- **`add`**: Property was added
|
|
737
|
+
- **`set`**: Property value was changed
|
|
738
|
+
- **`del`**: Property was deleted
|
|
739
|
+
- **`bunch`**: Collection operation (array methods, map/set operations)
|
|
740
|
+
|
|
741
|
+
```typescript
|
|
742
|
+
const obj = reactive({ count: 0 })
|
|
743
|
+
let state = getState(obj)
|
|
744
|
+
|
|
745
|
+
effect(() => {
|
|
746
|
+
let changes = []
|
|
747
|
+
while ('evolution' in state) {
|
|
748
|
+
changes.push(state.evolution)
|
|
749
|
+
state = state.next
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (changes.length > 0) {
|
|
753
|
+
console.log('Changes since last effect:', changes)
|
|
754
|
+
}
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
obj.count = 5 // { type: 'set', prop: 'count' }
|
|
758
|
+
obj.newProp = 'test' // { type: 'add', prop: 'newProp' }
|
|
759
|
+
delete obj.count // { type: 'del', prop: 'count' }
|
|
760
|
+
|
|
761
|
+
// Array operations
|
|
762
|
+
const array = reactive([1, 2, 3])
|
|
763
|
+
array.push(4) // { type: 'bunch', method: 'push' }
|
|
764
|
+
array.reverse() // { type: 'bunch', method: 'reverse' }
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
### Change History Patterns
|
|
768
|
+
|
|
769
|
+
Common patterns for using evolution tracking:
|
|
770
|
+
|
|
771
|
+
```typescript
|
|
772
|
+
// Pattern 1: Count changes
|
|
773
|
+
let state = getState(obj)
|
|
774
|
+
effect(() => {
|
|
775
|
+
let changes = 0
|
|
776
|
+
while ('evolution' in state) {
|
|
777
|
+
changes++
|
|
778
|
+
state = state.next
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (changes > 0) {
|
|
782
|
+
console.log(`Detected ${changes} changes`)
|
|
783
|
+
state = getState(obj) // Reset for next run
|
|
784
|
+
}
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
// Pattern 2: Filter specific changes
|
|
788
|
+
let state = getState(obj)
|
|
789
|
+
effect(() => {
|
|
790
|
+
const relevantChanges = []
|
|
791
|
+
while ('evolution' in state) {
|
|
792
|
+
if (state.evolution.type === 'set') {
|
|
793
|
+
relevantChanges.push(state.evolution)
|
|
794
|
+
}
|
|
795
|
+
state = state.next
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (relevantChanges.length > 0) {
|
|
799
|
+
console.log('Property updates:', relevantChanges)
|
|
800
|
+
state = getState(obj)
|
|
801
|
+
}
|
|
802
|
+
})
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
## Collections
|
|
806
|
+
|
|
807
|
+
### `ReactiveMap`
|
|
808
|
+
|
|
809
|
+
A reactive wrapper around JavaScript's `Map` class.
|
|
810
|
+
|
|
811
|
+
```typescript
|
|
812
|
+
const map = reactive(new Map([['key1', 'value1']]))
|
|
813
|
+
|
|
814
|
+
effect(() => {
|
|
815
|
+
console.log('Map size:', map.size)
|
|
816
|
+
console.log('Has key1:', map.has('key1'))
|
|
817
|
+
})
|
|
818
|
+
|
|
819
|
+
map.set('key2', 'value2') // Triggers effect
|
|
820
|
+
map.delete('key1') // Triggers effect
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
**Features:**
|
|
824
|
+
- Tracks `size` changes
|
|
825
|
+
- Tracks individual key operations
|
|
826
|
+
- Tracks collection-wide operations via `allProps`
|
|
827
|
+
|
|
828
|
+
### `ReactiveWeakMap`
|
|
829
|
+
|
|
830
|
+
A reactive wrapper around JavaScript's `WeakMap` class.
|
|
831
|
+
|
|
832
|
+
```typescript
|
|
833
|
+
const weakMap = reactive(new WeakMap())
|
|
834
|
+
const key = { id: 1 }
|
|
835
|
+
|
|
836
|
+
effect(() => {
|
|
837
|
+
console.log('Has key:', weakMap.has(key))
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
weakMap.set(key, 'value') // Triggers effect
|
|
841
|
+
weakMap.delete(key) // Triggers effect
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
**Features:**
|
|
845
|
+
- Only tracks individual key operations
|
|
846
|
+
- No `size` tracking (WeakMap limitation)
|
|
847
|
+
- No collection-wide operations
|
|
848
|
+
|
|
849
|
+
### `ReactiveSet`
|
|
850
|
+
|
|
851
|
+
A reactive wrapper around JavaScript's `Set` class.
|
|
852
|
+
|
|
853
|
+
```typescript
|
|
854
|
+
const set = reactive(new Set([1, 2, 3]))
|
|
855
|
+
|
|
856
|
+
effect(() => {
|
|
857
|
+
console.log('Set size:', set.size)
|
|
858
|
+
console.log('Has 1:', set.has(1))
|
|
859
|
+
})
|
|
860
|
+
|
|
861
|
+
set.add(4) // Triggers effect
|
|
862
|
+
set.delete(1) // Triggers effect
|
|
863
|
+
set.clear() // Triggers effect
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
**Features:**
|
|
867
|
+
- Tracks `size` changes
|
|
868
|
+
- Tracks individual value operations
|
|
869
|
+
- Tracks collection-wide operations
|
|
870
|
+
|
|
871
|
+
### `ReactiveWeakSet`
|
|
872
|
+
|
|
873
|
+
A reactive wrapper around JavaScript's `WeakSet` class.
|
|
874
|
+
|
|
875
|
+
```typescript
|
|
876
|
+
const weakSet = reactive(new WeakSet())
|
|
877
|
+
const obj = { id: 1 }
|
|
878
|
+
|
|
879
|
+
effect(() => {
|
|
880
|
+
console.log('Has obj:', weakSet.has(obj))
|
|
881
|
+
})
|
|
882
|
+
|
|
883
|
+
weakSet.add(obj) // Triggers effect
|
|
884
|
+
weakSet.delete(obj) // Triggers effect
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
### Collection-Specific Reactivity
|
|
888
|
+
|
|
889
|
+
Collections provide different levels of reactivity:
|
|
890
|
+
|
|
891
|
+
```typescript
|
|
892
|
+
const map = reactive(new Map())
|
|
893
|
+
|
|
894
|
+
// Size tracking
|
|
895
|
+
effect(() => {
|
|
896
|
+
console.log('Map size:', map.size)
|
|
897
|
+
})
|
|
898
|
+
|
|
899
|
+
// Individual key tracking
|
|
900
|
+
effect(() => {
|
|
901
|
+
console.log('Value for key1:', map.get('key1'))
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
// Collection-wide tracking
|
|
905
|
+
effect(() => {
|
|
906
|
+
for (const [key, value] of map) {
|
|
907
|
+
// This effect depends on allProps
|
|
908
|
+
}
|
|
909
|
+
})
|
|
910
|
+
|
|
911
|
+
// Operations trigger different effects
|
|
912
|
+
map.set('key1', 'value1') // Triggers size and key1 effects
|
|
913
|
+
map.set('key2', 'value2') // Triggers size and allProps effects
|
|
914
|
+
map.delete('key1') // Triggers size, key1, and allProps effects
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
## ReactiveArray
|
|
918
|
+
|
|
919
|
+
### `ReactiveArray`
|
|
920
|
+
|
|
921
|
+
A reactive wrapper around JavaScript's `Array` class with full array method support.
|
|
922
|
+
|
|
923
|
+
```typescript
|
|
924
|
+
const array = reactive([1, 2, 3])
|
|
925
|
+
|
|
926
|
+
effect(() => {
|
|
927
|
+
console.log('Array length:', array.length)
|
|
928
|
+
console.log('First element:', array[0])
|
|
929
|
+
})
|
|
930
|
+
|
|
931
|
+
array.push(4) // Triggers effect
|
|
932
|
+
array[0] = 10 // Triggers effect
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
**Features:**
|
|
936
|
+
- Tracks `length` changes
|
|
937
|
+
- Tracks individual index operations
|
|
938
|
+
- Tracks collection-wide operations via `allProps`
|
|
939
|
+
- Supports all array methods with proper reactivity
|
|
940
|
+
|
|
941
|
+
### Array Methods
|
|
942
|
+
|
|
943
|
+
All standard array methods are supported with reactivity:
|
|
944
|
+
|
|
945
|
+
```typescript
|
|
946
|
+
const array = reactive([1, 2, 3])
|
|
947
|
+
|
|
948
|
+
// Mutator methods
|
|
949
|
+
array.push(4) // Triggers length and allProps effects
|
|
950
|
+
array.pop() // Triggers length and allProps effects
|
|
951
|
+
array.shift() // Triggers length and allProps effects
|
|
952
|
+
array.unshift(0) // Triggers length and allProps effects
|
|
953
|
+
array.splice(1, 1, 10) // Triggers length and allProps effects
|
|
954
|
+
array.reverse() // Triggers allProps effects
|
|
955
|
+
array.sort() // Triggers allProps effects
|
|
956
|
+
array.fill(0) // Triggers allProps effects
|
|
957
|
+
array.copyWithin(0, 2) // Triggers allProps effects
|
|
958
|
+
|
|
959
|
+
// Accessor methods (immutable)
|
|
960
|
+
const reversed = array.toReversed()
|
|
961
|
+
const sorted = array.toSorted()
|
|
962
|
+
const spliced = array.toSpliced(1, 1)
|
|
963
|
+
const withNew = array.with(0, 100)
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
### Index Access
|
|
967
|
+
|
|
968
|
+
ReactiveArray supports both positive and negative index access:
|
|
969
|
+
|
|
970
|
+
```typescript
|
|
971
|
+
const array = reactive([1, 2, 3, 4, 5])
|
|
972
|
+
|
|
973
|
+
effect(() => {
|
|
974
|
+
console.log('First element:', array[0])
|
|
975
|
+
console.log('Last element:', array.at(-1))
|
|
976
|
+
})
|
|
977
|
+
|
|
978
|
+
array[0] = 10 // Triggers effect
|
|
979
|
+
array[4] = 50 // Triggers effect
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
### Length Reactivity
|
|
983
|
+
|
|
984
|
+
The `length` property is fully reactive:
|
|
985
|
+
|
|
986
|
+
```typescript
|
|
987
|
+
const array = reactive([1, 2, 3])
|
|
988
|
+
|
|
989
|
+
effect(() => {
|
|
990
|
+
console.log('Array length:', array.length)
|
|
991
|
+
})
|
|
992
|
+
|
|
993
|
+
array.push(4) // Triggers effect
|
|
994
|
+
array.length = 2 // Triggers effect
|
|
995
|
+
array[5] = 10 // Triggers effect (expands array)
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
### Array Evolution Tracking
|
|
999
|
+
|
|
1000
|
+
Array operations generate specific evolution events:
|
|
1001
|
+
|
|
1002
|
+
```typescript
|
|
1003
|
+
const array = reactive([1, 2, 3])
|
|
1004
|
+
let state = getState(array)
|
|
1005
|
+
|
|
1006
|
+
effect(() => {
|
|
1007
|
+
while ('evolution' in state) {
|
|
1008
|
+
console.log('Array change:', state.evolution)
|
|
1009
|
+
state = state.next
|
|
1010
|
+
}
|
|
1011
|
+
})
|
|
1012
|
+
|
|
1013
|
+
array.push(4) // { type: 'bunch', method: 'push' }
|
|
1014
|
+
array[0] = 10 // { type: 'set', prop: 0 }
|
|
1015
|
+
array[5] = 20 // { type: 'add', prop: 5 }
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
### Array-Specific Reactivity Patterns
|
|
1019
|
+
|
|
1020
|
+
```typescript
|
|
1021
|
+
const array = reactive([1, 2, 3])
|
|
1022
|
+
|
|
1023
|
+
// Track specific indices
|
|
1024
|
+
effect(() => {
|
|
1025
|
+
console.log('First two elements:', array[0], array[1])
|
|
1026
|
+
})
|
|
1027
|
+
|
|
1028
|
+
// Track length changes
|
|
1029
|
+
effect(() => {
|
|
1030
|
+
console.log('Array size changed to:', array.length)
|
|
1031
|
+
})
|
|
1032
|
+
|
|
1033
|
+
// Track all elements (via iteration)
|
|
1034
|
+
effect(() => {
|
|
1035
|
+
for (const item of array) {
|
|
1036
|
+
// This effect depends on allProps
|
|
1037
|
+
}
|
|
1038
|
+
})
|
|
1039
|
+
|
|
1040
|
+
// Track specific array methods
|
|
1041
|
+
effect(() => {
|
|
1042
|
+
const lastElement = array.at(-1)
|
|
1043
|
+
console.log('Last element:', lastElement)
|
|
1044
|
+
})
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
### Performance Considerations
|
|
1048
|
+
|
|
1049
|
+
ReactiveArray is optimized for common array operations:
|
|
1050
|
+
|
|
1051
|
+
```typescript
|
|
1052
|
+
// Efficient: Direct index access
|
|
1053
|
+
effect(() => {
|
|
1054
|
+
console.log(array[0]) // Only tracks index 0
|
|
1055
|
+
})
|
|
1056
|
+
|
|
1057
|
+
// Efficient: Length tracking
|
|
1058
|
+
effect(() => {
|
|
1059
|
+
console.log(array.length) // Only tracks length
|
|
1060
|
+
})
|
|
1061
|
+
|
|
1062
|
+
// Less efficient: Iteration tracks all elements
|
|
1063
|
+
effect(() => {
|
|
1064
|
+
array.forEach(item => console.log(item)) // Tracks allProps
|
|
1065
|
+
})
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
## Class Reactivity
|
|
1069
|
+
|
|
1070
|
+
### `@reactive` Decorator
|
|
1071
|
+
|
|
1072
|
+
The `@reactive` decorator makes class instances automatically reactive. This is the recommended approach for adding reactivity to classes.
|
|
1073
|
+
|
|
1074
|
+
```typescript
|
|
1075
|
+
import { reactive } from 'mutts/reactive'
|
|
1076
|
+
|
|
1077
|
+
@reactive
|
|
1078
|
+
class User {
|
|
1079
|
+
name: string
|
|
1080
|
+
age: number
|
|
1081
|
+
|
|
1082
|
+
constructor(name: string, age: number) {
|
|
1083
|
+
this.name = name
|
|
1084
|
+
this.age = age
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
updateAge(newAge: number) {
|
|
1088
|
+
this.age = newAge
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const user = new User("John", 30)
|
|
1093
|
+
|
|
1094
|
+
effect(() => {
|
|
1095
|
+
console.log(`User: ${user.name}, Age: ${user.age}`)
|
|
1096
|
+
})
|
|
1097
|
+
|
|
1098
|
+
user.updateAge(31) // Triggers effect
|
|
1099
|
+
user.name = "Jane" // Triggers effect
|
|
1100
|
+
```
|
|
1101
|
+
|
|
1102
|
+
### Functional Syntax
|
|
1103
|
+
|
|
1104
|
+
You can also use the functional syntax for making classes reactive:
|
|
1105
|
+
|
|
1106
|
+
```typescript
|
|
1107
|
+
import { reactive } from 'mutts/reactive'
|
|
1108
|
+
|
|
1109
|
+
class User {
|
|
1110
|
+
name: string
|
|
1111
|
+
age: number
|
|
1112
|
+
|
|
1113
|
+
constructor(name: string, age: number) {
|
|
1114
|
+
this.name = name
|
|
1115
|
+
this.age = age
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
updateAge(newAge: number) {
|
|
1119
|
+
this.age = newAge
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
const ReactiveUser = reactive(User)
|
|
1124
|
+
const user = new ReactiveUser("John", 30)
|
|
1125
|
+
|
|
1126
|
+
effect(() => {
|
|
1127
|
+
console.log(`User: ${user.name}, Age: ${user.age}`)
|
|
1128
|
+
})
|
|
1129
|
+
|
|
1130
|
+
user.updateAge(31) // Triggers effect
|
|
1131
|
+
user.name = "Jane" // Triggers effect
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
### `ReactiveBase` for Complex Inheritance
|
|
1135
|
+
|
|
1136
|
+
For complex inheritance trees, especially when you need to solve constructor reactivity issues, extend `ReactiveBase`:
|
|
1137
|
+
|
|
1138
|
+
```typescript
|
|
1139
|
+
import { ReactiveBase, reactive } from 'mutts/reactive'
|
|
1140
|
+
|
|
1141
|
+
class GameObject extends ReactiveBase {
|
|
1142
|
+
id = 'game-object'
|
|
1143
|
+
position = { x: 0, y: 0 }
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
class Entity extends GameObject {
|
|
1147
|
+
health = 100
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
@reactive
|
|
1151
|
+
class Player extends Entity {
|
|
1152
|
+
name = 'Player'
|
|
1153
|
+
level = 1
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const player = new Player()
|
|
1157
|
+
|
|
1158
|
+
effect(() => {
|
|
1159
|
+
console.log(`Player ${player.name} at (${player.position.x}, ${player.position.y})`)
|
|
1160
|
+
})
|
|
1161
|
+
|
|
1162
|
+
player.position.x = 10 // Triggers effect
|
|
1163
|
+
player.health = 80 // Triggers effect
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
**Advantages of `ReactiveBase`:**
|
|
1167
|
+
|
|
1168
|
+
1. **Constructor Reactivity**: Solves the issue where `this` in the constructor is not yet reactive
|
|
1169
|
+
2. **Inheritance Safety**: Prevents reactivity from being added to prototype chains in complex inheritance trees
|
|
1170
|
+
3. **No Side Effects**: The base class itself has no effect - it only enables proper reactivity when combined with `@reactive`
|
|
1171
|
+
|
|
1172
|
+
### Choosing the Right Approach
|
|
1173
|
+
|
|
1174
|
+
**Use `@reactive` decorator when:**
|
|
1175
|
+
- You have simple classes without complex inheritance
|
|
1176
|
+
- You want the cleanest, most modern syntax
|
|
1177
|
+
- You don't need to modify or use `this` in the constructor
|
|
1178
|
+
|
|
1179
|
+
**Use `ReactiveBase` + `@reactive` when:**
|
|
1180
|
+
- You have complex inheritance trees (like game objects, UI components)
|
|
1181
|
+
- You need to modify or use `this` in the constructor
|
|
1182
|
+
- You want to prevent reactivity from being added to prototype chains
|
|
1183
|
+
|
|
1184
|
+
### Making Existing Class Instances Reactive
|
|
1185
|
+
|
|
1186
|
+
You can also make existing class instances reactive:
|
|
1187
|
+
|
|
1188
|
+
```typescript
|
|
1189
|
+
class Counter {
|
|
1190
|
+
count = 0
|
|
1191
|
+
|
|
1192
|
+
increment() {
|
|
1193
|
+
this.count++
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const counter = new Counter()
|
|
1198
|
+
const reactiveCounter = reactive(counter)
|
|
1199
|
+
|
|
1200
|
+
effect(() => {
|
|
1201
|
+
console.log('Count:', reactiveCounter.count)
|
|
1202
|
+
})
|
|
1203
|
+
|
|
1204
|
+
reactiveCounter.increment() // Triggers effect
|
|
1205
|
+
```
|
|
1206
|
+
|
|
1207
|
+
### Method Reactivity
|
|
1208
|
+
|
|
1209
|
+
Methods that modify properties automatically trigger effects:
|
|
1210
|
+
|
|
1211
|
+
```typescript
|
|
1212
|
+
@reactive
|
|
1213
|
+
class ShoppingCart {
|
|
1214
|
+
items: string[] = []
|
|
1215
|
+
|
|
1216
|
+
addItem(item: string) {
|
|
1217
|
+
this.items.push(item)
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
removeItem(item: string) {
|
|
1221
|
+
const index = this.items.indexOf(item)
|
|
1222
|
+
if (index > -1) {
|
|
1223
|
+
this.items.splice(index, 1)
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const cart = new ShoppingCart()
|
|
1229
|
+
|
|
1230
|
+
effect(() => {
|
|
1231
|
+
console.log('Cart items:', cart.items)
|
|
1232
|
+
})
|
|
1233
|
+
|
|
1234
|
+
cart.addItem('Apple') // Triggers effect
|
|
1235
|
+
cart.removeItem('Apple') // Triggers effect
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
### Inheritance Support
|
|
1239
|
+
|
|
1240
|
+
The `@reactive` decorator works with inheritance:
|
|
1241
|
+
|
|
1242
|
+
```typescript
|
|
1243
|
+
@reactive
|
|
1244
|
+
class Animal {
|
|
1245
|
+
species: string
|
|
1246
|
+
|
|
1247
|
+
constructor(species: string) {
|
|
1248
|
+
this.species = species
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
class Dog extends Animal {
|
|
1253
|
+
breed: string
|
|
1254
|
+
|
|
1255
|
+
constructor(breed: string) {
|
|
1256
|
+
super('Canis')
|
|
1257
|
+
this.breed = breed
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const dog = new Dog('Golden Retriever')
|
|
1262
|
+
|
|
1263
|
+
effect(() => {
|
|
1264
|
+
console.log(`${dog.species}: ${dog.breed}`)
|
|
1265
|
+
})
|
|
1266
|
+
|
|
1267
|
+
dog.breed = 'Labrador' // Triggers effect
|
|
1268
|
+
```
|
|
1269
|
+
|
|
1270
|
+
**Note**: When using inheritance with the `@reactive` decorator, apply it to the base class. The decorator will automatically handle inheritance properly.
|
|
1271
|
+
|
|
1272
|
+
## Non-Reactive System
|
|
1273
|
+
|
|
1274
|
+
### `unreactive()`
|
|
1275
|
+
|
|
1276
|
+
Marks objects or classes as non-reactive, preventing them from being wrapped.
|
|
1277
|
+
|
|
1278
|
+
```typescript
|
|
1279
|
+
function unreactive<T>(target: T): T
|
|
1280
|
+
function unreactive(target: Constructor<T>): Constructor<T>
|
|
1281
|
+
```
|
|
1282
|
+
|
|
1283
|
+
**Examples:**
|
|
1284
|
+
|
|
1285
|
+
```typescript
|
|
1286
|
+
// Mark individual object as non-reactive
|
|
1287
|
+
const obj = { count: 0 }
|
|
1288
|
+
unreactive(obj)
|
|
1289
|
+
const reactiveObj = reactive(obj) // Returns obj unchanged
|
|
1290
|
+
|
|
1291
|
+
// Mark entire class as non-reactive
|
|
1292
|
+
class Utility {
|
|
1293
|
+
static helper() { return 'help' }
|
|
1294
|
+
}
|
|
1295
|
+
unreactive(Utility)
|
|
1296
|
+
const instance = new Utility()
|
|
1297
|
+
const reactiveInstance = reactive(instance) // Returns instance unchanged
|
|
1298
|
+
```
|
|
1299
|
+
|
|
1300
|
+
### `@unreactive` Decorator
|
|
1301
|
+
|
|
1302
|
+
Mark class properties as non-reactive.
|
|
1303
|
+
|
|
1304
|
+
```typescript
|
|
1305
|
+
@reactive
|
|
1306
|
+
class User {
|
|
1307
|
+
@unreactive
|
|
1308
|
+
id: string = 'user-123'
|
|
1309
|
+
|
|
1310
|
+
name: string = 'John'
|
|
1311
|
+
age: number = 30
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
const user = new User()
|
|
1315
|
+
|
|
1316
|
+
effect(() => {
|
|
1317
|
+
console.log(user.name, user.age) // Tracks these
|
|
1318
|
+
console.log(user.id) // Does NOT track this
|
|
1319
|
+
})
|
|
1320
|
+
|
|
1321
|
+
user.name = 'Jane' // Triggers effect
|
|
1322
|
+
user.id = 'new-id' // Does NOT trigger effect
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
### Non-Reactive Classes
|
|
1326
|
+
|
|
1327
|
+
Classes marked as non-reactive bypass the reactive system entirely:
|
|
1328
|
+
|
|
1329
|
+
```typescript
|
|
1330
|
+
class Config {
|
|
1331
|
+
apiUrl: string = 'https://api.example.com'
|
|
1332
|
+
timeout: number = 5000
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
unreactive(Config)
|
|
1336
|
+
```
|
|
1337
|
+
-or-
|
|
1338
|
+
```typescript
|
|
1339
|
+
@unreactive
|
|
1340
|
+
class Config {
|
|
1341
|
+
apiUrl: string = 'https://api.example.com'
|
|
1342
|
+
timeout: number = 5000
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const ReactiveConfig = reactive(Config)
|
|
1346
|
+
const config = new ReactiveConfig()
|
|
1347
|
+
|
|
1348
|
+
effect(() => {
|
|
1349
|
+
console.log('Config:', config.apiUrl, config.timeout)
|
|
1350
|
+
})
|
|
1351
|
+
|
|
1352
|
+
// These changes won't trigger effects
|
|
1353
|
+
config.apiUrl = 'https://new-api.example.com'
|
|
1354
|
+
config.timeout = 10000
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
### Making Special Reactive Objects Non-Reactive
|
|
1358
|
+
|
|
1359
|
+
Special reactive objects (Arrays, Maps, Sets, WeakMaps, WeakSets) can also be made non-reactive:
|
|
1360
|
+
|
|
1361
|
+
```typescript
|
|
1362
|
+
// Make individual reactive collections non-reactive
|
|
1363
|
+
const array = reactive([1, 2, 3])
|
|
1364
|
+
unreactive(array) // array is no longer reactive
|
|
1365
|
+
|
|
1366
|
+
const map = reactive(new Map([['key', 'value']]))
|
|
1367
|
+
unreactive(map) // map is no longer reactive
|
|
1368
|
+
|
|
1369
|
+
const set = reactive(new Set([1, 2, 3]))
|
|
1370
|
+
unreactive(set) // set is no longer reactive
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
**Making reactive collection classes non-reactive:**
|
|
1374
|
+
|
|
1375
|
+
```typescript
|
|
1376
|
+
// Make the entire ReactiveArray class non-reactive
|
|
1377
|
+
unreactive(ReactiveArray)
|
|
1378
|
+
|
|
1379
|
+
// Now all ReactiveArray instances will be non-reactive
|
|
1380
|
+
const array = reactive([1, 2, 3]) // Returns non-reactive array
|
|
1381
|
+
const reactiveArray = new ReactiveArray([1, 2, 3]) // Non-reactive instance
|
|
1382
|
+
|
|
1383
|
+
// Make other reactive collection classes non-reactive
|
|
1384
|
+
unreactive(ReactiveMap)
|
|
1385
|
+
unreactive(ReactiveSet)
|
|
1386
|
+
unreactive(ReactiveWeakMap)
|
|
1387
|
+
unreactive(ReactiveWeakSet)
|
|
1388
|
+
```
|
|
1389
|
+
|
|
1390
|
+
**Use cases for non-reactive collections:**
|
|
1391
|
+
- Large datasets that don't need reactivity
|
|
1392
|
+
- Performance-critical operations
|
|
1393
|
+
- Static configuration data
|
|
1394
|
+
- Temporary data structures
|
|
1395
|
+
|
|
1396
|
+
### Performance Considerations
|
|
1397
|
+
|
|
1398
|
+
Non-reactive objects can improve performance:
|
|
1399
|
+
|
|
1400
|
+
```typescript
|
|
1401
|
+
// Good: Mark large, rarely-changing objects as non-reactive
|
|
1402
|
+
const config = unreactive({
|
|
1403
|
+
apiEndpoints: { /* large config object */ },
|
|
1404
|
+
featureFlags: { /* many flags */ }
|
|
1405
|
+
})
|
|
1406
|
+
|
|
1407
|
+
// Good: Mark utility classes as non-reactive
|
|
1408
|
+
class MathUtils {
|
|
1409
|
+
static PI = 3.14159
|
|
1410
|
+
static square(x: number) { return x * x }
|
|
1411
|
+
}
|
|
1412
|
+
unreactive(MathUtils)
|
|
1413
|
+
|
|
1414
|
+
// Good: Mark properties that don't need reactivity
|
|
1415
|
+
class User {
|
|
1416
|
+
@unreactive
|
|
1417
|
+
metadata: any = {} // Large metadata object
|
|
1418
|
+
|
|
1419
|
+
name: string = 'John' // This should be reactive
|
|
1420
|
+
}
|
|
1421
|
+
```
|
|
1422
|
+
|
|
1423
|
+
## Computed Properties
|
|
1424
|
+
|
|
1425
|
+
### `@computed` Decorator
|
|
1426
|
+
|
|
1427
|
+
Creates computed properties that cache their values and only recompute when dependencies change.
|
|
1428
|
+
|
|
1429
|
+
```typescript
|
|
1430
|
+
@reactive
|
|
1431
|
+
class Calculator {
|
|
1432
|
+
@computed
|
|
1433
|
+
get area() {
|
|
1434
|
+
return this.width * this.height
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
width: number = 10
|
|
1438
|
+
height: number = 5
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
const calc = new Calculator()
|
|
1442
|
+
|
|
1443
|
+
effect(() => {
|
|
1444
|
+
console.log('Area:', calc.area)
|
|
1445
|
+
})
|
|
1446
|
+
|
|
1447
|
+
calc.width = 20 // Triggers effect, recomputes area
|
|
1448
|
+
calc.height = 10 // Triggers effect, recomputes area
|
|
1449
|
+
```
|
|
1450
|
+
|
|
1451
|
+
### Computed Functions
|
|
1452
|
+
|
|
1453
|
+
Create computed values outside of classes:
|
|
1454
|
+
|
|
1455
|
+
```typescript
|
|
1456
|
+
const state = reactive({ a: 1, b: 2 })
|
|
1457
|
+
|
|
1458
|
+
const sum = computed(() => state.a + state.b)
|
|
1459
|
+
const product = computed(() => state.a * state.b)
|
|
1460
|
+
|
|
1461
|
+
effect(() => {
|
|
1462
|
+
console.log('Sum:', sum, 'Product:', product)
|
|
1463
|
+
})
|
|
1464
|
+
|
|
1465
|
+
state.a = 5 // Both computed values update
|
|
1466
|
+
```
|
|
1467
|
+
|
|
1468
|
+
### Caching and Invalidation
|
|
1469
|
+
|
|
1470
|
+
Computed values are cached until their dependencies change:
|
|
1471
|
+
|
|
1472
|
+
```typescript
|
|
1473
|
+
const state = reactive({ x: 1, y: 2 })
|
|
1474
|
+
|
|
1475
|
+
let computeCount = 0
|
|
1476
|
+
const expensive = computed(() => {
|
|
1477
|
+
computeCount++
|
|
1478
|
+
console.log('Computing expensive value...')
|
|
1479
|
+
return Math.pow(state.x, state.y)
|
|
1480
|
+
})
|
|
1481
|
+
|
|
1482
|
+
console.log('First access:', expensive) // Computes once
|
|
1483
|
+
console.log('Second access:', expensive) // Uses cached value
|
|
1484
|
+
console.log('Compute count:', computeCount) // 1
|
|
1485
|
+
|
|
1486
|
+
state.x = 2 // Invalidates cache
|
|
1487
|
+
console.log('After change:', expensive) // Recomputes
|
|
1488
|
+
console.log('Compute count:', computeCount) // 2
|
|
1489
|
+
```
|
|
1490
|
+
|
|
1491
|
+
### Computed vs Effects
|
|
1492
|
+
|
|
1493
|
+
Choose between computed and effects based on your needs:
|
|
1494
|
+
|
|
1495
|
+
```typescript
|
|
1496
|
+
const state = reactive({ count: 0 })
|
|
1497
|
+
|
|
1498
|
+
// Use computed when you need a value
|
|
1499
|
+
const doubled = computed(() => state.count * 2)
|
|
1500
|
+
|
|
1501
|
+
// Use effect when you need side effects
|
|
1502
|
+
effect(() => {
|
|
1503
|
+
console.log('Count doubled:', doubled)
|
|
1504
|
+
document.title = `Count: ${state.count}`
|
|
1505
|
+
})
|
|
1506
|
+
|
|
1507
|
+
state.count = 5
|
|
1508
|
+
// doubled becomes 10
|
|
1509
|
+
// effect logs and updates title
|
|
1510
|
+
```
|
|
1511
|
+
|
|
1512
|
+
## Advanced Patterns
|
|
1513
|
+
|
|
1514
|
+
### Custom Reactive Objects
|
|
1515
|
+
|
|
1516
|
+
Create custom reactive objects with specialized behavior:
|
|
1517
|
+
|
|
1518
|
+
```typescript
|
|
1519
|
+
class ReactiveArray<T> {
|
|
1520
|
+
private items: T[] = []
|
|
1521
|
+
|
|
1522
|
+
push(item: T) {
|
|
1523
|
+
this.items.push(item)
|
|
1524
|
+
touched(this, 'length')
|
|
1525
|
+
touched(this, 'allProps')
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
get length() {
|
|
1529
|
+
dependant(this, 'length')
|
|
1530
|
+
return this.items.length
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
get(index: number) {
|
|
1534
|
+
dependant(this, index)
|
|
1535
|
+
return this.items[index]
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
const array = new ReactiveArray<number>()
|
|
1540
|
+
effect(() => {
|
|
1541
|
+
console.log('Array length:', array.length)
|
|
1542
|
+
})
|
|
1543
|
+
|
|
1544
|
+
array.push(1) // Triggers effect
|
|
1545
|
+
```
|
|
1546
|
+
|
|
1547
|
+
### Native Reactivity Registration
|
|
1548
|
+
|
|
1549
|
+
Register custom reactive classes for automatic wrapping:
|
|
1550
|
+
|
|
1551
|
+
```typescript
|
|
1552
|
+
import { registerNativeReactivity } from 'mutts/reactive'
|
|
1553
|
+
|
|
1554
|
+
class CustomMap<K, V> {
|
|
1555
|
+
private data = new Map<K, V>()
|
|
1556
|
+
|
|
1557
|
+
set(key: K, value: V) {
|
|
1558
|
+
this.data.set(key, value)
|
|
1559
|
+
// Custom reactivity logic
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
get(key: K) {
|
|
1563
|
+
return this.data.get(key)
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
class ReactiveCustomMap<K, V> extends CustomMap<K, V> {
|
|
1568
|
+
// Reactive wrapper implementation
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
registerNativeReactivity(CustomMap, ReactiveCustomMap)
|
|
1572
|
+
|
|
1573
|
+
// Now CustomMap instances are automatically wrapped
|
|
1574
|
+
const customMap = reactive(new CustomMap())
|
|
1575
|
+
```
|
|
1576
|
+
|
|
1577
|
+
### Memory Management
|
|
1578
|
+
|
|
1579
|
+
The reactive system uses WeakMaps to avoid memory leaks:
|
|
1580
|
+
|
|
1581
|
+
```typescript
|
|
1582
|
+
// Objects can be garbage collected when no longer referenced
|
|
1583
|
+
let obj = { data: 'large object' }
|
|
1584
|
+
const reactiveObj = reactive(obj)
|
|
1585
|
+
|
|
1586
|
+
effect(() => {
|
|
1587
|
+
console.log(reactiveObj.data)
|
|
1588
|
+
})
|
|
1589
|
+
|
|
1590
|
+
// Remove reference
|
|
1591
|
+
obj = null
|
|
1592
|
+
reactiveObj = null
|
|
1593
|
+
|
|
1594
|
+
// The original object and its reactive wrapper can be GC'd
|
|
1595
|
+
```
|
|
1596
|
+
|
|
1597
|
+
### Performance Optimization
|
|
1598
|
+
|
|
1599
|
+
Optimize reactive performance:
|
|
1600
|
+
|
|
1601
|
+
```typescript
|
|
1602
|
+
// 1. Use non-reactive for static data
|
|
1603
|
+
const staticConfig = unreactive({
|
|
1604
|
+
apiUrl: 'https://api.example.com',
|
|
1605
|
+
version: '1.0.0'
|
|
1606
|
+
})
|
|
1607
|
+
|
|
1608
|
+
// 2. Batch changes when possible
|
|
1609
|
+
const state = reactive({ a: 1, b: 2, c: 3 })
|
|
1610
|
+
|
|
1611
|
+
// Instead of:
|
|
1612
|
+
state.a = 10
|
|
1613
|
+
state.b = 20
|
|
1614
|
+
state.c = 30
|
|
1615
|
+
|
|
1616
|
+
// Consider batching or using a single update method
|
|
1617
|
+
|
|
1618
|
+
// 3. Avoid unnecessary reactivity
|
|
1619
|
+
@reactive
|
|
1620
|
+
class User {
|
|
1621
|
+
@unreactive
|
|
1622
|
+
private internalId = crypto.randomUUID() // Never changes
|
|
1623
|
+
|
|
1624
|
+
name: string = 'John' // Changes, should be reactive
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// 4. Use appropriate collection types
|
|
1628
|
+
const smallSet = new ReactiveSet(new Set()) // For small collections
|
|
1629
|
+
const largeMap = new ReactiveMap(new Map()) // For large collections with key access
|
|
1630
|
+
```
|
|
1631
|
+
|
|
1632
|
+
## Debugging and Development
|
|
1633
|
+
|
|
1634
|
+
### Debug Options
|
|
1635
|
+
|
|
1636
|
+
Configure debug behavior:
|
|
1637
|
+
|
|
1638
|
+
```typescript
|
|
1639
|
+
import { options as reactiveOptions } from 'mutts/reactive'
|
|
1640
|
+
|
|
1641
|
+
// Track effect entry/exit
|
|
1642
|
+
reactiveOptions.enter = (effect) => {
|
|
1643
|
+
console.log('🔵 Entering effect:', effect.name || 'anonymous')
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
reactiveOptions.leave = (effect) => {
|
|
1647
|
+
console.log('🔴 Leaving effect:', effect.name || 'anonymous')
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// Track effect chaining
|
|
1651
|
+
reactiveOptions.chain = (caller, target) => {
|
|
1652
|
+
console.log('⛓️ Effect chain:', caller.name || 'anonymous', '->', target.name || 'anonymous')
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// Set maximum chain depth
|
|
1656
|
+
reactiveOptions.maxEffectChain = 50
|
|
1657
|
+
|
|
1658
|
+
// Set maximum deep watch traversal depth
|
|
1659
|
+
reactiveOptions.maxDeepWatchDepth = 200
|
|
1660
|
+
```
|
|
1661
|
+
|
|
1662
|
+
### Effect Stack Traces
|
|
1663
|
+
|
|
1664
|
+
Debug effect execution:
|
|
1665
|
+
|
|
1666
|
+
```typescript
|
|
1667
|
+
const state = reactive({ count: 0 })
|
|
1668
|
+
|
|
1669
|
+
effect(() => {
|
|
1670
|
+
console.trace('Effect running')
|
|
1671
|
+
console.log('Count:', state.count)
|
|
1672
|
+
})
|
|
1673
|
+
|
|
1674
|
+
// This will show the call stack when the effect runs
|
|
1675
|
+
state.count = 5
|
|
1676
|
+
```
|
|
1677
|
+
|
|
1678
|
+
### Evolution Inspection
|
|
1679
|
+
|
|
1680
|
+
Inspect object evolution history:
|
|
1681
|
+
|
|
1682
|
+
```typescript
|
|
1683
|
+
const obj = reactive({ x: 1 })
|
|
1684
|
+
let state = getState(obj)
|
|
1685
|
+
|
|
1686
|
+
effect(() => {
|
|
1687
|
+
console.log('=== Evolution History ===')
|
|
1688
|
+
let depth = 0
|
|
1689
|
+
while ('evolution' in state) {
|
|
1690
|
+
console.log(`${' '.repeat(depth)}${state.evolution.type}: ${state.evolution.prop}`)
|
|
1691
|
+
state = state.next
|
|
1692
|
+
depth++
|
|
1693
|
+
}
|
|
1694
|
+
console.log('=== End History ===')
|
|
1695
|
+
|
|
1696
|
+
// Reset state reference
|
|
1697
|
+
state = getState(obj)
|
|
1698
|
+
})
|
|
1699
|
+
|
|
1700
|
+
obj.x = 2
|
|
1701
|
+
obj.y = 'new'
|
|
1702
|
+
delete obj.x
|
|
1703
|
+
```
|
|
1704
|
+
|
|
1705
|
+
## API Reference
|
|
1706
|
+
|
|
1707
|
+
### Decorators
|
|
1708
|
+
|
|
1709
|
+
#### `@computed`
|
|
1710
|
+
|
|
1711
|
+
Marks a class accessor as computed. The computed value will be cached and invalidated when dependencies change.
|
|
1712
|
+
|
|
1713
|
+
```typescript
|
|
1714
|
+
class MyClass {
|
|
1715
|
+
private _value = 0;
|
|
1716
|
+
|
|
1717
|
+
@computed
|
|
1718
|
+
get doubled() {
|
|
1719
|
+
return this._value * 2;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// Function usage
|
|
1724
|
+
function myExpensiveCalculus() {
|
|
1725
|
+
|
|
1726
|
+
}
|
|
1727
|
+
...
|
|
1728
|
+
const result = computed(myExpensiveCalculus);
|
|
1729
|
+
```
|
|
1730
|
+
|
|
1731
|
+
**Use Cases:**
|
|
1732
|
+
- Caching expensive calculations
|
|
1733
|
+
- Derived state that depends on reactive values
|
|
1734
|
+
- Computed properties in classes
|
|
1735
|
+
- Performance optimization for frequently accessed values
|
|
1736
|
+
|
|
1737
|
+
**Notes:**
|
|
1738
|
+
By how JS works, writing `computed(()=> ...)` will always be wrong, as the notation `()=> ...` internally is a `new Function(...)`.
|
|
1739
|
+
So, even if the return value is cached, it will never be used.
|
|
1740
|
+
|
|
1741
|
+
#### `@unreactive`
|
|
1742
|
+
|
|
1743
|
+
Marks a class property as non-reactive. The property change will not be tracked by the reactive system.
|
|
1744
|
+
|
|
1745
|
+
Marks a class (and its descendants) as non-reactive.
|
|
1746
|
+
|
|
1747
|
+
```typescript
|
|
1748
|
+
class MyClass {
|
|
1749
|
+
@unreactive
|
|
1750
|
+
private config = { theme: 'dark' };
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// Class decorator usage
|
|
1754
|
+
@unreactive
|
|
1755
|
+
class NonReactiveClass {
|
|
1756
|
+
// All instances will be non-reactive
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// Function usage
|
|
1760
|
+
const nonReactiveObj = unreactive({ config: { theme: 'dark' } });
|
|
1761
|
+
```
|
|
1762
|
+
|
|
1763
|
+
**Use Cases:**
|
|
1764
|
+
- Configuration objects that shouldn't trigger reactivity
|
|
1765
|
+
- Static data that never changes
|
|
1766
|
+
- Performance optimization for objects that don't need tracking
|
|
1767
|
+
- Third-party objects that shouldn't be made reactive
|
|
1768
|
+
|
|
1769
|
+
### Core Functions
|
|
1770
|
+
|
|
1771
|
+
#### `reactive<T>(target: T): T`
|
|
1772
|
+
|
|
1773
|
+
Creates a reactive proxy of the target object. All property access and mutations will be tracked.
|
|
1774
|
+
|
|
1775
|
+
**Use Cases:**
|
|
1776
|
+
- Converting plain objects to reactive objects
|
|
1777
|
+
- Making class instances reactive
|
|
1778
|
+
- Creating reactive arrays, maps, and sets
|
|
1779
|
+
- Setting up reactive state management
|
|
1780
|
+
|
|
1781
|
+
```typescript
|
|
1782
|
+
const state = reactive({ count: 0, name: 'John' });
|
|
1783
|
+
const items = reactive([1, 2, 3]);
|
|
1784
|
+
const map = reactive(new Map([['key', 'value']]));
|
|
1785
|
+
```
|
|
1786
|
+
|
|
1787
|
+
#### `effect(fn, ...args): ScopedCallback`
|
|
1788
|
+
|
|
1789
|
+
Creates a reactive effect that runs when its dependencies change.
|
|
1790
|
+
|
|
1791
|
+
**Use Cases:**
|
|
1792
|
+
- Side effects like DOM updates
|
|
1793
|
+
- Logging and debugging
|
|
1794
|
+
- Data synchronization
|
|
1795
|
+
- Cleanup operations
|
|
1796
|
+
|
|
1797
|
+
```typescript
|
|
1798
|
+
const cleanup = effect((dep) => {
|
|
1799
|
+
console.log('Count changed:', state.count);
|
|
1800
|
+
return () => {
|
|
1801
|
+
// Cleanup function
|
|
1802
|
+
};
|
|
1803
|
+
});
|
|
1804
|
+
```
|
|
1805
|
+
|
|
1806
|
+
#### `computed<T>(getter: ComputedFunction<T>): T`
|
|
1807
|
+
|
|
1808
|
+
Creates a computed value that caches its result and recomputes when dependencies change.
|
|
1809
|
+
|
|
1810
|
+
**Use Cases:**
|
|
1811
|
+
- Derived state calculations
|
|
1812
|
+
- Expensive computations
|
|
1813
|
+
- Data transformations
|
|
1814
|
+
- Conditional logic based on reactive state
|
|
1815
|
+
|
|
1816
|
+
```typescript
|
|
1817
|
+
const result = computed(someExpensiveCalculus);
|
|
1818
|
+
```
|
|
1819
|
+
|
|
1820
|
+
#### `watch(value, callback, options?): ScopedCallback`
|
|
1821
|
+
|
|
1822
|
+
Watches a reactive value or function and calls a callback when it changes.
|
|
1823
|
+
|
|
1824
|
+
**Use Cases:**
|
|
1825
|
+
- Reacting to specific value changes
|
|
1826
|
+
- Debugging reactive state
|
|
1827
|
+
- Side effects for specific properties
|
|
1828
|
+
- Data validation
|
|
1829
|
+
|
|
1830
|
+
```typescript
|
|
1831
|
+
// Watch a specific value
|
|
1832
|
+
const stop = watch(() => state.count, (newVal, oldVal) => {
|
|
1833
|
+
console.log(`Count changed from ${oldVal} to ${newVal}`);
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1836
|
+
// Watch an object with deep option
|
|
1837
|
+
const stopDeep = watch(state, (newState) => {
|
|
1838
|
+
console.log('State changed:', newState);
|
|
1839
|
+
}, { deep: true, immediate: true });
|
|
1840
|
+
```
|
|
1841
|
+
|
|
1842
|
+
#### `unwrap<T>(proxy: T): T`
|
|
1843
|
+
|
|
1844
|
+
Returns the original object from a reactive proxy.
|
|
1845
|
+
|
|
1846
|
+
**Use Cases:**
|
|
1847
|
+
- Accessing original object for serialization
|
|
1848
|
+
- Passing to non-reactive functions
|
|
1849
|
+
- Performance optimization
|
|
1850
|
+
- Debugging
|
|
1851
|
+
|
|
1852
|
+
```typescript
|
|
1853
|
+
const original = unwrap(reactiveState);
|
|
1854
|
+
JSON.stringify(original); // Safe to serialize
|
|
1855
|
+
```
|
|
1856
|
+
|
|
1857
|
+
#### `isReactive(obj: any): boolean`
|
|
1858
|
+
|
|
1859
|
+
Checks if an object is a reactive proxy.
|
|
1860
|
+
|
|
1861
|
+
**Use Cases:**
|
|
1862
|
+
- Type checking
|
|
1863
|
+
- Debugging
|
|
1864
|
+
- Conditional logic
|
|
1865
|
+
- Validation
|
|
1866
|
+
|
|
1867
|
+
```typescript
|
|
1868
|
+
if (isReactive(obj)) {
|
|
1869
|
+
console.log('Object is reactive');
|
|
1870
|
+
}
|
|
1871
|
+
```
|
|
1872
|
+
|
|
1873
|
+
#### `isNonReactive(obj: any): boolean`
|
|
1874
|
+
|
|
1875
|
+
Checks if an object is marked as non-reactive.
|
|
1876
|
+
|
|
1877
|
+
**Use Cases:**
|
|
1878
|
+
- Validation
|
|
1879
|
+
- Debugging
|
|
1880
|
+
- Conditional logic
|
|
1881
|
+
- Type checking
|
|
1882
|
+
|
|
1883
|
+
```typescript
|
|
1884
|
+
if (isNonReactive(obj)) {
|
|
1885
|
+
console.log('Object is non-reactive');
|
|
1886
|
+
}
|
|
1887
|
+
```
|
|
1888
|
+
|
|
1889
|
+
#### `untracked(fn: () => void): void`
|
|
1890
|
+
|
|
1891
|
+
Executes a function without tracking dependencies.
|
|
1892
|
+
|
|
1893
|
+
**Use Cases:**
|
|
1894
|
+
- Performance optimization
|
|
1895
|
+
- Avoiding circular dependencies
|
|
1896
|
+
- Side effects that shouldn't trigger reactivity
|
|
1897
|
+
- Batch operations
|
|
1898
|
+
|
|
1899
|
+
```typescript
|
|
1900
|
+
untracked(() => {
|
|
1901
|
+
// This won't create dependencies
|
|
1902
|
+
console.log('Untracked operation');
|
|
1903
|
+
});
|
|
1904
|
+
```
|
|
1905
|
+
|
|
1906
|
+
#### `getState(obj)`
|
|
1907
|
+
|
|
1908
|
+
Gets the current state of a reactive object. Used internally for tracking changes.
|
|
1909
|
+
|
|
1910
|
+
**Use Cases:**
|
|
1911
|
+
- Debugging reactive state
|
|
1912
|
+
- Custom reactive implementations
|
|
1913
|
+
- State inspection
|
|
1914
|
+
|
|
1915
|
+
#### `invalidateComputed(callback, warn?)`
|
|
1916
|
+
|
|
1917
|
+
Registers a callback to be called when a computed property is invalidated.
|
|
1918
|
+
|
|
1919
|
+
**Use Cases:**
|
|
1920
|
+
- Custom computed implementations
|
|
1921
|
+
- Cleanup operations
|
|
1922
|
+
- Performance monitoring
|
|
1923
|
+
|
|
1924
|
+
```typescript
|
|
1925
|
+
const computed = computed(() => {
|
|
1926
|
+
invalidateComputed(() => {
|
|
1927
|
+
console.log('Computed invalidated');
|
|
1928
|
+
});
|
|
1929
|
+
return expensiveCalculation();
|
|
1930
|
+
});
|
|
1931
|
+
```
|
|
1932
|
+
|
|
1933
|
+
### Configuration
|
|
1934
|
+
|
|
1935
|
+
#### `reactiveOptions`
|
|
1936
|
+
|
|
1937
|
+
Global options for the reactive system.
|
|
1938
|
+
|
|
1939
|
+
**Properties:**
|
|
1940
|
+
- `enter(effect: Function)`: Called when an effect is entered
|
|
1941
|
+
- `leave(effect: Function)`: Called when an effect is left
|
|
1942
|
+
- `chain(target: Function, caller?: Function)`: Called when effects are chained
|
|
1943
|
+
- `maxEffectChain: number`: Maximum effect chain depth (default: 100)
|
|
1944
|
+
- `maxDeepWatchDepth: number`: Maximum deep watch traversal depth (default: 100)
|
|
1945
|
+
- `instanceMembers: boolean`: Only react on instance members (default: true)
|
|
1946
|
+
- `warn(...args: any[])`: Warning function (default: console.warn)
|
|
1947
|
+
|
|
1948
|
+
**Use Cases:**
|
|
1949
|
+
- Debugging reactive behavior
|
|
1950
|
+
- Performance tuning
|
|
1951
|
+
- Custom logging
|
|
1952
|
+
- Error handling
|
|
1953
|
+
|
|
1954
|
+
```typescript
|
|
1955
|
+
reactiveOptions.maxEffectChain = 50;
|
|
1956
|
+
reactiveOptions.enter = (effect) => console.log('Effect entered:', effect.name);
|
|
1957
|
+
```
|
|
1958
|
+
|
|
1959
|
+
### Classes
|
|
1960
|
+
|
|
1961
|
+
#### `ReactiveBase`
|
|
1962
|
+
|
|
1963
|
+
Base class for reactive objects. When extended, instances are automatically made reactive.
|
|
1964
|
+
|
|
1965
|
+
**Use Cases:**
|
|
1966
|
+
- Creating reactive classes
|
|
1967
|
+
- Automatic reactivity for class instances
|
|
1968
|
+
- Type safety for reactive objects
|
|
1969
|
+
|
|
1970
|
+
```typescript
|
|
1971
|
+
class MyState extends ReactiveBase {
|
|
1972
|
+
count = 0;
|
|
1973
|
+
name = '';
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
const state = new MyState(); // Automatically reactive
|
|
1977
|
+
```
|
|
1978
|
+
|
|
1979
|
+
#### `ReactiveError`
|
|
1980
|
+
|
|
1981
|
+
Error class for reactive system errors.
|
|
1982
|
+
|
|
1983
|
+
**Use Cases:**
|
|
1984
|
+
- Error handling in reactive code
|
|
1985
|
+
- Debugging reactive issues
|
|
1986
|
+
- Custom error types
|
|
1987
|
+
|
|
1988
|
+
### Collections
|
|
1989
|
+
|
|
1990
|
+
Collections (Array, Map, Set, WeakMap, WeakSet) can be automatically made reactive when passed to `reactive()`:
|
|
1991
|
+
|
|
1992
|
+
```typescript
|
|
1993
|
+
const items = reactive([1, 2, 3]);
|
|
1994
|
+
items.push(4); // Triggers reactivity
|
|
1995
|
+
|
|
1996
|
+
const map = reactive(new Map([['key', 'value']]));
|
|
1997
|
+
map.set('newKey', 'newValue'); // Triggers reactivity
|
|
1998
|
+
|
|
1999
|
+
const set = reactive(new Set([1, 2, 3]));
|
|
2000
|
+
set.add(4); // Triggers reactivity
|
|
2001
|
+
```
|
|
2002
|
+
|
|
2003
|
+
#### Automatic Collection Reactivity
|
|
2004
|
+
|
|
2005
|
+
All native collections have their specific management:
|
|
2006
|
+
|
|
2007
|
+
```typescript
|
|
2008
|
+
// Collections still need to be wrapped with reactive()
|
|
2009
|
+
const arr = reactive([1, 2, 3]) // ReactiveArray
|
|
2010
|
+
const map = reactive(new Map()) // ReactiveMap
|
|
2011
|
+
const set = reactive(new Set()) // ReactiveSet
|
|
2012
|
+
const weakMap = reactive(new WeakMap()) // ReactiveWeakMap
|
|
2013
|
+
const weakSet = reactive(new WeakSet()) // ReactiveWeakSet
|
|
2014
|
+
|
|
2015
|
+
effect(() => {
|
|
2016
|
+
console.log('Array length:', arr.length)
|
|
2017
|
+
console.log('Map size:', map.size)
|
|
2018
|
+
console.log('Set size:', set.size)
|
|
2019
|
+
})
|
|
2020
|
+
|
|
2021
|
+
arr.push(4) // Triggers effect
|
|
2022
|
+
map.set('key', 'value') // Triggers effect
|
|
2023
|
+
set.add('item') // Triggers effect
|
|
2024
|
+
```
|
|
2025
|
+
|
|
2026
|
+
**Use Cases:**
|
|
2027
|
+
- Applications that primarily work with reactive collections
|
|
2028
|
+
- Global reactive state management
|
|
2029
|
+
- Ensuring collection methods (push, set, add, etc.) trigger reactivity
|
|
2030
|
+
- Performance optimization for collection-heavy applications
|
|
2031
|
+
|
|
2032
|
+
**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.
|
|
2033
|
+
|
|
2034
|
+
### Types
|
|
2035
|
+
|
|
2036
|
+
#### `ScopedCallback`
|
|
2037
|
+
|
|
2038
|
+
Type for effect cleanup functions.
|
|
2039
|
+
|
|
2040
|
+
#### `DependencyFunction`
|
|
2041
|
+
|
|
2042
|
+
Type for dependency tracking functions used in effects and computed values.
|
|
2043
|
+
|
|
2044
|
+
#### `WatchOptions`
|
|
2045
|
+
|
|
2046
|
+
Options for the watch function:
|
|
2047
|
+
- `immediate?: boolean`: Call callback immediately
|
|
2048
|
+
- `deep?: boolean`: Watch nested properties
|
|
2049
|
+
|
|
2050
|
+
### Profile Information
|
|
2051
|
+
|
|
2052
|
+
#### `profileInfo`
|
|
2053
|
+
|
|
2054
|
+
Object containing internal reactive system state for debugging and profiling.
|
|
2055
|
+
|
|
2056
|
+
**Properties:**
|
|
2057
|
+
- `objectToProxy`: WeakMap of original objects to their proxies
|
|
2058
|
+
- `proxyToObject`: WeakMap of proxies to their original objects
|
|
2059
|
+
- `effectToReactiveObjects`: WeakMap of effects to watched objects
|
|
2060
|
+
- `watchers`: WeakMap of objects to their property watchers
|
|
2061
|
+
- `objectParents`: WeakMap of objects to their parent relationships
|
|
2062
|
+
- `objectsWithDeepWatchers`: WeakSet of objects with deep watchers
|
|
2063
|
+
- `deepWatchers`: WeakMap of objects to their deep watchers
|
|
2064
|
+
- `effectToDeepWatchedObjects`: WeakMap of effects to deep watched objects
|
|
2065
|
+
- `nonReactiveObjects`: WeakSet of non-reactive objects
|
|
2066
|
+
- `computedCache`: WeakMap of computed functions to their cached values
|
|
2067
|
+
|
|
2068
|
+
**Use Cases:**
|
|
2069
|
+
- Debugging reactive behavior
|
|
2070
|
+
- Performance profiling
|
|
2071
|
+
- Memory leak detection
|
|
2072
|
+
- System state inspection
|