@zeix/cause-effect 0.17.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ai-context.md +19 -5
- package/.cursorrules +8 -3
- package/.github/copilot-instructions.md +9 -4
- package/CLAUDE.md +96 -262
- package/README.md +232 -421
- package/archive/computed.ts +2 -2
- package/archive/memo.ts +3 -2
- package/archive/task.ts +2 -2
- package/index.dev.js +59 -26
- package/index.js +1 -1
- package/index.ts +11 -3
- package/package.json +1 -1
- package/src/classes/collection.ts +38 -28
- package/src/classes/computed.ts +3 -3
- package/src/classes/list.ts +8 -7
- package/src/classes/ref.ts +68 -0
- package/src/errors.ts +21 -0
- package/src/match.ts +5 -12
- package/src/resolve.ts +3 -2
- package/src/util.ts +0 -4
- package/test/collection.test.ts +104 -47
- package/test/ref.test.ts +227 -0
- package/types/index.d.ts +5 -4
- package/types/src/classes/collection.d.ts +21 -7
- package/types/src/classes/list.d.ts +4 -4
- package/types/src/classes/ref.d.ts +39 -0
- package/types/src/errors.d.ts +6 -1
- package/types/src/util.d.ts +1 -2
package/.ai-context.md
CHANGED
|
@@ -9,10 +9,11 @@ Cause & Effect is a modern reactive state management library for JavaScript/Type
|
|
|
9
9
|
### Signal Types
|
|
10
10
|
- **Signal**: Base interface with `.get()` method for value access
|
|
11
11
|
- **State**: Mutable signals for primitive values (numbers, strings, booleans)
|
|
12
|
+
- **Ref**: Signal wrappers for external objects that change outside the reactive system (DOM elements, Map, Set, Date, third-party objects)
|
|
13
|
+
- **Computed**: Read-only derived signals with automatic memoization, reducer capabilities and async handling
|
|
12
14
|
- **Store**: Mutable signals for objects with individually reactive properties
|
|
13
15
|
- **List**: Mutable signals for arrays with individually reactive items
|
|
14
|
-
- **Collection**:
|
|
15
|
-
- **Computed**: Read-only derived signals with automatic memoization, reducer capabilities and asyc handling
|
|
16
|
+
- **Collection**: Interface for reactive array-like collections (implemented by DerivedCollection)
|
|
16
17
|
- **Effect**: Side effect handlers that react to signal changes
|
|
17
18
|
|
|
18
19
|
### Key Principles
|
|
@@ -29,9 +30,10 @@ cause-effect/
|
|
|
29
30
|
├── src/
|
|
30
31
|
│ ├── classes/
|
|
31
32
|
│ │ ├── state.ts # Mutable state signals (State)
|
|
33
|
+
│ │ ├── ref.ts # Signal wrapper for external objects (Ref)
|
|
32
34
|
│ │ ├── store.ts # Object stores with reactive properties
|
|
33
35
|
│ │ ├── list.ts # Array stores with stable keys (List)
|
|
34
|
-
│ │ ├── collection.ts #
|
|
36
|
+
│ │ ├── collection.ts # Collection interface and DerivedCollection
|
|
35
37
|
│ │ └── computed.ts # Computed/derived signals (Memo and Task)
|
|
36
38
|
| ├── signal.ts # Base signal types and utilities
|
|
37
39
|
│ ├── effect.ts # Effect system (createEffect)
|
|
@@ -55,6 +57,10 @@ const count = new State(42)
|
|
|
55
57
|
const name = new State('Alice')
|
|
56
58
|
const actions = new State<'increment' | 'decrement' | 'reset'>('reset')
|
|
57
59
|
|
|
60
|
+
// Ref signals for external objects
|
|
61
|
+
const elementRef = new Ref(document.getElementById('status'))
|
|
62
|
+
const cacheRef = new Ref(new Map([['key', 'value']]))
|
|
63
|
+
|
|
58
64
|
// Store signals for objects
|
|
59
65
|
const user = createStore({ name: 'Alice', age: 30 })
|
|
60
66
|
|
|
@@ -181,15 +187,19 @@ const firstTodo = todoList.byKey('task1')
|
|
|
181
187
|
firstTodo?.completed.set(true) // Mark completed
|
|
182
188
|
|
|
183
189
|
// Collections for read-only derived data
|
|
184
|
-
const completedTodos = new
|
|
190
|
+
const completedTodos = new DerivedCollection(todoList, todo =>
|
|
185
191
|
todo.completed ? { ...todo, status: 'done' } : null
|
|
186
192
|
).filter(Boolean) // Remove null values
|
|
187
193
|
|
|
188
194
|
// Async collections for enhanced data
|
|
189
|
-
const todoWithDetails = new
|
|
195
|
+
const todoWithDetails = new DerivedCollection(todoList, async (todo, abort) => {
|
|
190
196
|
const response = await fetch(`/todos/${todo.id}/details`, { signal: abort })
|
|
191
197
|
return { ...todo, details: await response.json() }
|
|
192
198
|
})
|
|
199
|
+
|
|
200
|
+
// Element collections for DOM reactivity
|
|
201
|
+
const buttons = createElementCollection(document.body, 'button')
|
|
202
|
+
const inputs = createElementCollection(form, 'input[type="text"]')
|
|
193
203
|
```
|
|
194
204
|
|
|
195
205
|
### Derived State
|
|
@@ -265,6 +275,10 @@ const processedItems = items.deriveCollection(item => ({
|
|
|
265
275
|
const finalResults = processedItems.deriveCollection(item =>
|
|
266
276
|
item.processed ? { summary: `${item.name} at ${item.timestamp}` } : null
|
|
267
277
|
).filter(Boolean)
|
|
278
|
+
|
|
279
|
+
// Ref signal manual notifications
|
|
280
|
+
elementRef.notify() // Notify when DOM element changes externally
|
|
281
|
+
cacheRef.notify() // Notify when Map/Set changes externally
|
|
268
282
|
```
|
|
269
283
|
|
|
270
284
|
## Build and Development
|
package/.cursorrules
CHANGED
|
@@ -6,9 +6,11 @@ TypeScript/JavaScript reactive state management library using signals pattern
|
|
|
6
6
|
## Core Concepts
|
|
7
7
|
- Signals: Base reactive primitives with .get() method
|
|
8
8
|
- State: new State() for primitives/simple values
|
|
9
|
+
- Ref: new Ref() for external objects (DOM, Map, Set) requiring manual .notify()
|
|
9
10
|
- Computed: new Memo() for derived/memoized values and new Task() for asynchronous computations
|
|
10
11
|
- Store: createStore() for objects with reactive properties
|
|
11
12
|
- List: new List() for arrays with stable keys and reactive items
|
|
13
|
+
- Collection: Interface for reactive collections (DerivedCollection, ElementCollection)
|
|
12
14
|
- Effects: createEffect() for side effects
|
|
13
15
|
|
|
14
16
|
## Code Style
|
|
@@ -31,9 +33,12 @@ TypeScript/JavaScript reactive state management library using signals pattern
|
|
|
31
33
|
|
|
32
34
|
## File Structure
|
|
33
35
|
- src/signal.ts - Base signal types
|
|
34
|
-
- src/state.ts - Mutable state signals
|
|
35
|
-
- src/
|
|
36
|
-
- src/
|
|
36
|
+
- src/classes/state.ts - Mutable state signals
|
|
37
|
+
- src/classes/ref.ts - Signal wrappers for external objects
|
|
38
|
+
- src/classes/store.ts - Object stores
|
|
39
|
+
- src/classes/list.ts - Array stores with stable keys
|
|
40
|
+
- src/classes/collection.ts - Collection interface and DerivedCollection
|
|
41
|
+
- src/classes/computed.ts - Computed signals
|
|
37
42
|
- src/effect.ts - Effect system
|
|
38
43
|
- src/system.ts - Core reactivity (watchers, batching)
|
|
39
44
|
- index.ts - Main exports
|
|
@@ -8,18 +8,20 @@ Cause & Effect is a reactive state management library for JavaScript/TypeScript
|
|
|
8
8
|
|
|
9
9
|
- **Signals**: Base reactive primitives with `.get()` method
|
|
10
10
|
- **State**: Mutable signals for primitive values (`new State()`)
|
|
11
|
+
- **Ref**: Signal wrappers for external objects that change outside reactive system (`new Ref()`)
|
|
11
12
|
- **Computed**: Derived read-only signals with memoization, reducer capabilities and async support (`new Memo()`, `new Task()`)
|
|
12
13
|
- **Store**: Mutable signals for objects with reactive properties (`createStore()`)
|
|
13
14
|
- **List**: Mutable signals for arrays with stable keys and reactive entries (`new List()`)
|
|
14
|
-
- **Collection**:
|
|
15
|
+
- **Collection**: Interface for reactive collections (implemented by `DerivedCollection`)
|
|
15
16
|
- **Effects**: Side effect handlers that react to signal changes (`createEffect()`)
|
|
16
17
|
|
|
17
18
|
## Key Files Structure
|
|
18
19
|
|
|
19
20
|
- `src/classes/state.ts` - Mutable state signals
|
|
21
|
+
- `src/classes/ref.ts` - Signal wrappers for external objects (DOM, Map, Set, etc.)
|
|
20
22
|
- `src/classes/store.ts` - Object stores with reactive properties
|
|
21
23
|
- `src/classes/list.ts` - Array stores with stable keys and reactive items
|
|
22
|
-
- `src/classes/collection.ts` -
|
|
24
|
+
- `src/classes/collection.ts` - Collection interface and DerivedCollection implementation
|
|
23
25
|
- `src/classes/computed.ts` - Computed/derived signals
|
|
24
26
|
- `src/signal.ts` - Base signal types and utilities
|
|
25
27
|
- `src/effect.ts` - Effect system
|
|
@@ -77,6 +79,10 @@ const count = new State(42)
|
|
|
77
79
|
const name = new State('Alice')
|
|
78
80
|
const actions = new State<'increment' | 'decrement'>('increment')
|
|
79
81
|
|
|
82
|
+
// Ref for external objects
|
|
83
|
+
const elementRef = new Ref(document.getElementById('status'))
|
|
84
|
+
const cacheRef = new Ref(new Map())
|
|
85
|
+
|
|
80
86
|
// Store for objects
|
|
81
87
|
const user = createStore({ name: 'Alice', age: 30 })
|
|
82
88
|
|
|
@@ -145,7 +151,6 @@ function isSignal<T extends {}>(value: unknown): value is Signal<T>
|
|
|
145
151
|
const items = new List(['a', 'b', 'c'])
|
|
146
152
|
|
|
147
153
|
// String prefix keys
|
|
148
|
-
// Lists with stable keys
|
|
149
154
|
const items = new List(['apple', 'banana'], 'fruit')
|
|
150
155
|
// Creates keys: 'fruit0', 'fruit1'
|
|
151
156
|
|
|
@@ -156,7 +161,7 @@ const users = new List([
|
|
|
156
161
|
], user => user.id) // Uses user.id as stable key
|
|
157
162
|
|
|
158
163
|
// Collections derived from lists
|
|
159
|
-
const userProfiles = new
|
|
164
|
+
const userProfiles = new DerivedCollection(users, user => ({
|
|
160
165
|
...user,
|
|
161
166
|
displayName: `${user.name} (ID: ${user.id})`
|
|
162
167
|
}))
|
package/CLAUDE.md
CHANGED
|
@@ -6,21 +6,20 @@ Cause & Effect is a reactive state management library that implements the signal
|
|
|
6
6
|
|
|
7
7
|
### Core Philosophy
|
|
8
8
|
- **Granular Reactivity**: Changes propagate only to dependent computations, minimizing unnecessary work
|
|
9
|
-
- **Explicit Dependencies**:
|
|
10
|
-
- **
|
|
11
|
-
- **Type Safety First**: Comprehensive TypeScript support with strict generic constraints
|
|
9
|
+
- **Explicit Dependencies**: Dependencies are tracked through `.get()` calls, no hidden subscriptions
|
|
10
|
+
- **Type Safety First**: Comprehensive TypeScript support with strict generic constraints (`T extends {}`)
|
|
12
11
|
- **Performance Conscious**: Minimal overhead through efficient dependency tracking and batching
|
|
13
12
|
|
|
14
13
|
## Mental Model for Understanding the System
|
|
15
14
|
|
|
16
15
|
Think of signals as **observable cells** in a spreadsheet:
|
|
17
|
-
- **State signals** are
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
- **State signals** are input cells for primitive values and objects
|
|
17
|
+
- **Ref signals** are cells that reference external objects (DOM, Map, Set) requiring manual notification
|
|
18
|
+
- **Computed signals** are formula cells that automatically recalculate when dependencies change
|
|
19
|
+
- **Store signals** are structured tables where individual columns (properties) are reactive
|
|
20
|
+
- **List signals** are tables with stable row IDs that survive sorting and reordering
|
|
21
|
+
- **Collection signals** are read-only derived tables with item-level memoization
|
|
22
|
+
- **Effects** are event handlers that trigger side effects when cells change
|
|
24
23
|
|
|
25
24
|
## Architectural Deep Dive
|
|
26
25
|
|
|
@@ -45,72 +44,52 @@ interface MutableSignal<T extends {}> extends Signal<T> {
|
|
|
45
44
|
update(fn: (current: T) => T): void
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
// Collection
|
|
49
|
-
interface Collection<T extends {}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
// Collection interface - implemented by various collection types
|
|
48
|
+
interface Collection<T extends {}> {
|
|
49
|
+
get(): T[]
|
|
50
|
+
at(index: number): Signal<T> | undefined
|
|
51
|
+
byKey(key: string): Signal<T> | undefined
|
|
52
|
+
deriveCollection<R extends {}>(callback: CollectionCallback<R, T>): Collection<R>
|
|
53
53
|
}
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
The generic constraint `T extends {}` is crucial - it excludes `null` and `undefined` at the type level, preventing common runtime errors and making the API more predictable.
|
|
57
57
|
|
|
58
|
-
### Collection
|
|
59
|
-
|
|
60
|
-
Collection signals provide read-only derived array transformations with automatic memoization:
|
|
61
|
-
|
|
62
|
-
1. **Source Dependency**: Collections derive from Lists or other Collections
|
|
63
|
-
2. **Item-level Memoization**: Each item transformation is individually memoized
|
|
64
|
-
3. **Async Support**: Supports Promise-based transformations with cancellation
|
|
65
|
-
4. **Stable Keys**: Maintains the same stable key system as source Lists
|
|
66
|
-
5. **Chainable**: Collections can derive from other Collections for data pipelines
|
|
67
|
-
|
|
68
|
-
Key implementation details:
|
|
69
|
-
- Individual items are Computed signals, not mutable signals
|
|
70
|
-
- Automatically handles source List changes (add, remove, sort)
|
|
71
|
-
- Supports both sync and async transformation callbacks
|
|
72
|
-
- Lazy evaluation - transformations only run when accessed
|
|
73
|
-
- Smart watcher management - only creates watchers when change listeners are active
|
|
74
|
-
- Order preservation through internal `#order` array synchronized with source
|
|
58
|
+
### Collection Architecture
|
|
75
59
|
|
|
76
|
-
|
|
77
|
-
- **BaseCollection**: Core implementation with signal management and event handling
|
|
78
|
-
- **Computed Creation**: Each source item gets its own computed signal with transformation callback
|
|
79
|
-
- **Source Synchronization**: Listens to source events and maintains parallel structure
|
|
80
|
-
- **Memory Efficiency**: Automatically cleans up computed signals when source items are removed
|
|
60
|
+
Collections are an interface implemented by different reactive array types:
|
|
81
61
|
|
|
82
|
-
|
|
62
|
+
**DerivedCollection**: Read-only transformations of Lists or other Collections
|
|
63
|
+
- Item-level memoization with Computed signals
|
|
64
|
+
- Async support with automatic cancellation
|
|
65
|
+
- Chainable for data pipelines
|
|
83
66
|
|
|
84
|
-
|
|
67
|
+
**ElementCollection**: DOM element collections with MutationObserver
|
|
68
|
+
- Uses Ref signals for elements that change externally
|
|
69
|
+
- Watches attributes and childList mutations
|
|
70
|
+
- Stable keys for persistent element identity
|
|
85
71
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
72
|
+
Key patterns:
|
|
73
|
+
- Collections return arrays of values via `.get()`
|
|
74
|
+
- Individual items accessed as signals via `.at()` and `.byKey()`
|
|
75
|
+
- All collections support `.deriveCollection()` for chaining
|
|
90
76
|
|
|
91
|
-
|
|
92
|
-
- Uses Proxy to intercept property access
|
|
93
|
-
- Maintains internal signal instances for each property
|
|
94
|
-
- Supports change notifications for fine-grained updates
|
|
95
|
-
- Handles edge cases like symbol properties and prototype chain
|
|
96
|
-
- For lists: maintains stable keys that persist through sorting, splicing, and reordering
|
|
97
|
-
- Provides specialized methods: `byKey()`, `keyAt()`, `indexOfKey()`, `splice()` for array manipulation
|
|
77
|
+
### Store and List Architecture
|
|
98
78
|
|
|
99
|
-
|
|
79
|
+
**Store signals** (`createStore`): Transform objects into reactive data structures
|
|
80
|
+
- Each property becomes its own signal via Proxy
|
|
81
|
+
- Built on `Composite` class for signal management
|
|
82
|
+
- Dynamic property addition/removal with proper reactivity
|
|
100
83
|
|
|
101
|
-
|
|
84
|
+
**List signals** (`new List`): Arrays with stable keys and reactive items
|
|
85
|
+
- Maintains stable keys that survive sorting and reordering
|
|
86
|
+
- Built on `Composite` class for consistent signal management
|
|
87
|
+
- Provides `byKey()`, `keyAt()`, `indexOfKey()` for key-based access
|
|
102
88
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
5. **Performance**: Only creates signals when properties are first accessed or added
|
|
108
|
-
|
|
109
|
-
Key implementation details:
|
|
110
|
-
- Lazy signal creation for better memory usage
|
|
111
|
-
- Efficient batch updates through change detection
|
|
112
|
-
- Support for both object and array data structures
|
|
113
|
-
- Automatic cleanup when properties are removed
|
|
89
|
+
**Composite Architecture**: Shared foundation for Store and List
|
|
90
|
+
- `Map<string, Signal>` for property/item signals
|
|
91
|
+
- Event system for granular add/change/remove notifications
|
|
92
|
+
- Lazy signal creation and automatic cleanup
|
|
114
93
|
|
|
115
94
|
### Computed Signal Memoization Strategy
|
|
116
95
|
|
|
@@ -134,51 +113,41 @@ Computed signals implement smart memoization:
|
|
|
134
113
|
```typescript
|
|
135
114
|
const count = new State(0)
|
|
136
115
|
const theme = new State<'light' | 'dark'>('light')
|
|
137
|
-
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Ref Signals (`new Ref`)**:
|
|
119
|
+
- External objects that change outside the reactive system
|
|
120
|
+
- DOM elements, Map, Set, Date objects, third-party APIs
|
|
121
|
+
- Requires manual `.notify()` when external object changes
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
const elementRef = new Ref(document.getElementById('status'))
|
|
125
|
+
const cacheRef = new Ref(new Map())
|
|
126
|
+
// When external change occurs: cacheRef.notify()
|
|
138
127
|
```
|
|
139
128
|
|
|
140
129
|
**Store Signals (`createStore`)**:
|
|
141
130
|
- Objects where individual properties change independently
|
|
142
|
-
- Nested data structures
|
|
143
|
-
- Form state where fields update individually
|
|
144
|
-
- Configuration objects with multiple settings
|
|
145
131
|
|
|
146
132
|
```typescript
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
password: '',
|
|
150
|
-
errors: { email: null, password: null }
|
|
151
|
-
})
|
|
152
|
-
// form.email.set('user@example.com') // Only email subscribers react
|
|
133
|
+
const user = createStore({ name: 'Alice', email: 'alice@example.com' })
|
|
134
|
+
user.name.set('Bob') // Only name subscribers react
|
|
153
135
|
```
|
|
154
136
|
|
|
155
137
|
**List Signals (`new List`)**:
|
|
156
|
-
|
|
157
|
-
```ts
|
|
158
|
-
// Lists with stable keys
|
|
138
|
+
```typescript
|
|
159
139
|
const todoList = new List([
|
|
160
|
-
{ id: 'task1', text: 'Learn signals' }
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
// Access by stable key instead of index
|
|
165
|
-
const firstTodo = todoList.byKey('task1')
|
|
166
|
-
firstTodo?.text.set('Learn signals deeply')
|
|
167
|
-
|
|
168
|
-
// Sort while maintaining stable references
|
|
169
|
-
todoList.sort((a, b) => a.text.localeCompare(b.text))
|
|
140
|
+
{ id: 'task1', text: 'Learn signals' }
|
|
141
|
+
], todo => todo.id)
|
|
142
|
+
const firstTodo = todoList.byKey('task1') // Access by stable key
|
|
170
143
|
```
|
|
171
144
|
|
|
172
|
-
**Collection Signals (`new
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
// Read-only derived arrays with memoization
|
|
176
|
-
const completedTodos = new Collection(todoList, todo =>
|
|
145
|
+
**Collection Signals (`new DerivedCollection`)**:
|
|
146
|
+
```typescript
|
|
147
|
+
const completedTodos = new DerivedCollection(todoList, todo =>
|
|
177
148
|
todo.completed ? { ...todo, status: 'done' } : null
|
|
178
149
|
)
|
|
179
|
-
|
|
180
|
-
// Async transformations with cancellation
|
|
181
|
-
const todoDetails = new Collection(todoList, async (todo, abort) => {
|
|
150
|
+
const todoDetails = new DerivedCollection(todoList, async (todo, abort) => {
|
|
182
151
|
const response = await fetch(`/todos/${todo.id}`, { signal: abort })
|
|
183
152
|
return { ...todo, details: await response.json() }
|
|
184
153
|
})
|
|
@@ -245,19 +214,13 @@ The library provides several layers of error handling:
|
|
|
245
214
|
4. **Helper Functions**: `resolve()` and `match()` for ergonomic error handling
|
|
246
215
|
|
|
247
216
|
```typescript
|
|
248
|
-
//
|
|
217
|
+
// Error handling with resolve() and match()
|
|
249
218
|
const apiData = new Task(async (prev, abort) => {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return response.json()
|
|
254
|
-
} catch (error) {
|
|
255
|
-
if (abort.aborted) return prev // Cancelled, not an error
|
|
256
|
-
throw error // Real error
|
|
257
|
-
}
|
|
219
|
+
const response = await fetch('/api/data', { signal: abort })
|
|
220
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
221
|
+
return response.json()
|
|
258
222
|
})
|
|
259
223
|
|
|
260
|
-
// Pattern matching for comprehensive state handling
|
|
261
224
|
createEffect(() => {
|
|
262
225
|
match(resolve({ apiData }), {
|
|
263
226
|
ok: ({ apiData }) => updateUI(apiData),
|
|
@@ -267,199 +230,70 @@ createEffect(() => {
|
|
|
267
230
|
})
|
|
268
231
|
```
|
|
269
232
|
|
|
270
|
-
### Performance Optimization
|
|
271
|
-
|
|
272
|
-
1. **Batching Updates**: Use `batchSignalWrites()` for multiple synchronous updates
|
|
273
|
-
2. **Selective Dependencies**: Structure computed signals to minimize dependencies
|
|
274
|
-
3. **Cleanup Management**: Properly dispose of effects to prevent memory leaks
|
|
275
|
-
4. **Shallow vs Deep Equality**: Use appropriate comparison strategies
|
|
233
|
+
### Performance Optimization
|
|
276
234
|
|
|
235
|
+
**Batching**: Use `batchSignalWrites()` for multiple updates
|
|
277
236
|
```typescript
|
|
278
|
-
// Create a user store
|
|
279
|
-
const user = createStore<{ id: number, name: string, email: string, age?: number }>({
|
|
280
|
-
id: 1,
|
|
281
|
-
name: 'John Doe',
|
|
282
|
-
email: 'john@example.com'
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
// Batch multiple updates to prevent intermediate states
|
|
286
237
|
batchSignalWrites(() => {
|
|
287
|
-
user.name.set('Alice
|
|
288
|
-
user.age.set(30)
|
|
238
|
+
user.name.set('Alice')
|
|
289
239
|
user.email.set('alice@example.com')
|
|
290
|
-
}) //
|
|
291
|
-
|
|
292
|
-
// Optimize computed dependencies
|
|
293
|
-
const userDisplay = new Memo(() => {
|
|
294
|
-
const { name, email } = user.get() // Gets entire object, will rerun also if age changes
|
|
295
|
-
return `${name} <${email}>`
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
// Better: more granular dependencies
|
|
299
|
-
const userDisplay = new Memo(() => {
|
|
300
|
-
return `${user.name.get()} <${user.email.get()}>` // Only depends on name/email
|
|
301
|
-
})
|
|
240
|
+
}) // Single effect trigger
|
|
302
241
|
```
|
|
303
242
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
- **Vanilla JS**: Direct DOM manipulation in effects
|
|
312
|
-
|
|
313
|
-
### Testing Strategies
|
|
314
|
-
- **Unit Testing**: Test signal logic in isolation
|
|
315
|
-
- **Integration Testing**: Test effect chains and async operations
|
|
316
|
-
- **Mock Testing**: Mock external dependencies in computed signals
|
|
317
|
-
- **Property Testing**: Use random inputs to verify invariants
|
|
318
|
-
|
|
319
|
-
### Debugging Techniques
|
|
320
|
-
- Use `resolve()` to inspect multiple signal states at once
|
|
321
|
-
- Add logging effects for debugging reactivity chains
|
|
322
|
-
- Use AbortSignal inspection for async operation debugging
|
|
323
|
-
- Leverage TypeScript for compile-time dependency analysis
|
|
243
|
+
**Granular Dependencies**: Structure computed signals to minimize dependencies
|
|
244
|
+
```typescript
|
|
245
|
+
// Bad: depends on entire user object
|
|
246
|
+
const display = new Memo(() => user.get().name + user.get().email)
|
|
247
|
+
// Good: only depends on specific properties
|
|
248
|
+
const display = new Memo(() => user.name.get() + user.email.get())
|
|
249
|
+
```
|
|
324
250
|
|
|
325
|
-
## Common Pitfalls
|
|
251
|
+
## Common Pitfalls
|
|
326
252
|
|
|
327
253
|
1. **Infinite Loops**: Don't update signals within their own computed callbacks
|
|
328
|
-
2. **
|
|
329
|
-
3. **
|
|
330
|
-
4. **
|
|
331
|
-
5. **Async Race Conditions**: Trust the automatic cancellation, don't fight it
|
|
254
|
+
2. **Memory Leaks**: Clean up effects when components unmount
|
|
255
|
+
3. **Over-reactivity**: Structure data to minimize unnecessary updates
|
|
256
|
+
4. **Async Race Conditions**: Trust automatic cancellation with AbortSignal
|
|
332
257
|
|
|
333
|
-
## Advanced
|
|
258
|
+
## Advanced Patterns
|
|
334
259
|
|
|
335
|
-
###
|
|
260
|
+
### Event Bus with Type Safety
|
|
336
261
|
```typescript
|
|
337
|
-
|
|
338
|
-
const todos = new List<Todo[]>([])
|
|
339
|
-
const completedCount = new Memo(() =>
|
|
340
|
-
todos.get().filter(todo => todo.completed).length
|
|
341
|
-
)
|
|
342
|
-
const remainingCount = new Memo(() =>
|
|
343
|
-
todos.get().length - completedCount.get()
|
|
344
|
-
)
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
### Reactive State Machines
|
|
348
|
-
```typescript
|
|
349
|
-
const state = new State<'idle' | 'loading' | 'success' | 'error'>('idle')
|
|
350
|
-
const canRetry = () => state.get() === 'error'
|
|
351
|
-
const isLoading = () => state.get() === 'loading'
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
### Cross-Component Communication
|
|
355
|
-
```typescript
|
|
356
|
-
// Component ownership pattern: The component that emits events owns the store
|
|
357
|
-
// Component B (the emitter) declares the shape of events it will emit upfront
|
|
358
|
-
type EventBusSchema = {
|
|
262
|
+
type Events = {
|
|
359
263
|
userLogin: { userId: number; timestamp: number }
|
|
360
264
|
userLogout: { userId: number }
|
|
361
|
-
userUpdate: { userId: number; profile: { name: string } }
|
|
362
265
|
}
|
|
363
266
|
|
|
364
|
-
|
|
365
|
-
const eventBus = createStore<EventBusSchema>({
|
|
267
|
+
const eventBus = createStore<Events>({
|
|
366
268
|
userLogin: UNSET,
|
|
367
|
-
userLogout: UNSET
|
|
368
|
-
userUpdate: UNSET,
|
|
269
|
+
userLogout: UNSET
|
|
369
270
|
})
|
|
370
271
|
|
|
371
|
-
|
|
372
|
-
const emit = <K extends keyof EventBusSchema>(
|
|
373
|
-
event: K,
|
|
374
|
-
data: EventBusSchema[K],
|
|
375
|
-
) => {
|
|
272
|
+
const emit = <K extends keyof Events>(event: K, data: Events[K]) => {
|
|
376
273
|
eventBus[event].set(data)
|
|
377
274
|
}
|
|
378
275
|
|
|
379
|
-
|
|
380
|
-
const on = <K extends keyof EventBusSchema>(
|
|
381
|
-
event: K,
|
|
382
|
-
callback: (data: EventBusSchema[K]) => void,
|
|
383
|
-
) =>
|
|
276
|
+
const on = <K extends keyof Events>(event: K, callback: (data: Events[K]) => void) =>
|
|
384
277
|
createEffect(() => {
|
|
385
278
|
const data = eventBus[event].get()
|
|
386
279
|
if (data !== UNSET) callback(data)
|
|
387
280
|
})
|
|
388
|
-
|
|
389
|
-
// Usage example:
|
|
390
|
-
// Component A listens for user events
|
|
391
|
-
on('userLogin', (data) => {
|
|
392
|
-
console.log('User logged in:', data) // data is properly typed
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
// Component B emits user events
|
|
396
|
-
eventBus.userLogin.set({ userId: 123, timestamp: Date.now() })
|
|
397
281
|
```
|
|
398
282
|
|
|
399
|
-
###
|
|
400
|
-
|
|
283
|
+
### Data Processing Pipelines
|
|
401
284
|
```typescript
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
{ id: 1, value: 10, category: 'A' },
|
|
405
|
-
{ id: 2, value: 20, category: 'B' },
|
|
406
|
-
{ id: 3, value: 15, category: 'A' }
|
|
407
|
-
])
|
|
408
|
-
|
|
409
|
-
// Multi-step transformation pipeline
|
|
410
|
-
const processedData = rawData
|
|
285
|
+
const rawData = new List([{ id: 1, value: 10 }])
|
|
286
|
+
const processed = rawData
|
|
411
287
|
.deriveCollection(item => ({ ...item, doubled: item.value * 2 }))
|
|
412
|
-
.deriveCollection(item => ({ ...item,
|
|
413
|
-
|
|
414
|
-
const categoryTotals = new Collection(processedData, async (item, abort) => {
|
|
415
|
-
// Simulate async processing
|
|
416
|
-
await new Promise(resolve => setTimeout(resolve, 100))
|
|
417
|
-
return { category: item.category, contribution: item.doubled }
|
|
418
|
-
})
|
|
419
|
-
|
|
420
|
-
// Aggregation can be done with computed signals
|
|
421
|
-
const totals = new Memo(() => {
|
|
422
|
-
const items = categoryTotals.get()
|
|
423
|
-
return items.reduce((acc, item) => {
|
|
424
|
-
acc[item.category] = (acc[item.category] || 0) + item.contribution
|
|
425
|
-
return acc
|
|
426
|
-
}, {} as Record<string, number>)
|
|
427
|
-
})
|
|
288
|
+
.deriveCollection(item => ({ ...item, formatted: `$${item.doubled}` }))
|
|
428
289
|
```
|
|
429
290
|
|
|
430
|
-
### Stable Keys
|
|
291
|
+
### Stable List Keys
|
|
431
292
|
```typescript
|
|
432
|
-
// Managing a list of items where order matters but identity persists
|
|
433
293
|
const playlist = new List([
|
|
434
|
-
{ id: 'track1', title: 'Song A'
|
|
435
|
-
{ id: 'track2', title: 'Song B', duration: 210 }
|
|
294
|
+
{ id: 'track1', title: 'Song A' }
|
|
436
295
|
], track => track.id)
|
|
437
296
|
|
|
438
|
-
|
|
439
|
-
const firstTrackSignal = playlist.byKey('track1')
|
|
440
|
-
const firstTrack = firstTrackSignal?.get()
|
|
441
|
-
|
|
442
|
-
// Reorder playlist while maintaining references
|
|
297
|
+
const firstTrack = playlist.byKey('track1') // Persists through sorting
|
|
443
298
|
playlist.sort((a, b) => a.title.localeCompare(b.title))
|
|
444
|
-
// firstTrackSignal still points to the same track!
|
|
445
|
-
|
|
446
|
-
// Insert new track at specific position
|
|
447
|
-
playlist.splice(1, 0, { id: 'track3', title: 'Song C', duration: 195 })
|
|
448
|
-
|
|
449
|
-
// Find current position of a track by its stable key
|
|
450
|
-
const track1Position = playlist.indexOfKey('track1')
|
|
451
|
-
const keyAtPosition2 = playlist.keyAt(2)
|
|
452
299
|
```
|
|
453
|
-
|
|
454
|
-
**Component ownership principle**: The component that emits events should own and initialize the event store. This creates clear boundaries and prevents coupling issues.
|
|
455
|
-
|
|
456
|
-
**Why this pattern?**: By having the owning component (Component B) initialize all known events with `UNSET`, we get:
|
|
457
|
-
1. **Fine-grained reactivity**: Each `on()` call establishes a direct dependency on the specific event signal
|
|
458
|
-
2. **Full type safety**: Event names and data shapes are constrained at compile time
|
|
459
|
-
3. **Simple logic**: No conditional updates, no store-wide watching in `on()`
|
|
460
|
-
4. **Clear ownership**: Component B declares its event contract upfront with a schema
|
|
461
|
-
5. **Better performance**: Only the specific event listeners trigger, not all listeners
|
|
462
|
-
6. **Explicit dependencies**: Effects track exactly which events they care about
|
|
463
|
-
7. **IntelliSense support**: IDEs can provide autocomplete for event names and data structures
|
|
464
|
-
|
|
465
|
-
This context should help you understand not just what the code does, but why it's structured this way and how to use it effectively in real-world applications.
|