@zeix/cause-effect 0.15.1 → 0.16.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/.ai-context.md +254 -0
- package/.cursorrules +54 -0
- package/.github/copilot-instructions.md +132 -0
- package/CLAUDE.md +319 -0
- package/README.md +167 -159
- package/eslint.config.js +1 -1
- package/index.dev.js +528 -407
- package/index.js +1 -1
- package/index.ts +36 -25
- package/package.json +1 -1
- package/src/computed.ts +41 -30
- package/src/diff.ts +57 -44
- package/src/effect.ts +15 -16
- package/src/errors.ts +64 -0
- package/src/match.ts +2 -2
- package/src/resolve.ts +2 -2
- package/src/signal.ts +27 -49
- package/src/state.ts +27 -19
- package/src/store.ts +410 -209
- package/src/system.ts +122 -0
- package/src/util.ts +45 -6
- package/test/batch.test.ts +18 -11
- package/test/benchmark.test.ts +4 -4
- package/test/computed.test.ts +508 -72
- package/test/diff.test.ts +321 -4
- package/test/effect.test.ts +61 -61
- package/test/match.test.ts +38 -28
- package/test/resolve.test.ts +16 -16
- package/test/signal.test.ts +19 -147
- package/test/state.test.ts +212 -25
- package/test/store.test.ts +1370 -134
- package/test/util/dependency-graph.ts +1 -1
- package/types/index.d.ts +10 -9
- package/types/src/collection.d.ts +26 -0
- package/types/src/computed.d.ts +9 -9
- package/types/src/diff.d.ts +5 -3
- package/types/src/effect.d.ts +3 -3
- package/types/src/errors.d.ts +22 -0
- package/types/src/match.d.ts +1 -1
- package/types/src/resolve.d.ts +1 -1
- package/types/src/signal.d.ts +12 -19
- package/types/src/state.d.ts +5 -5
- package/types/src/store.d.ts +40 -36
- package/types/src/system.d.ts +44 -0
- package/types/src/util.d.ts +7 -5
- package/index.d.ts +0 -36
- package/src/scheduler.ts +0 -172
- package/types/test-new-effect.d.ts +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Cause & Effect
|
|
2
2
|
|
|
3
|
-
Version 0.
|
|
3
|
+
Version 0.16.0
|
|
4
4
|
|
|
5
5
|
**Cause & Effect** is a lightweight, reactive state management library for JavaScript applications. It uses fine-grained reactivity with signals to create predictable and efficient data flow in your app.
|
|
6
6
|
|
|
@@ -10,10 +10,10 @@ Version 0.15.1
|
|
|
10
10
|
|
|
11
11
|
### Core Concepts
|
|
12
12
|
|
|
13
|
-
- **State signals**: Hold values that can be directly modified: `
|
|
14
|
-
- **Store signals**: Hold objects of nested reactive properties: `
|
|
15
|
-
- **Computed signals**: Derive memoized values from other signals: `
|
|
16
|
-
- **Effects**: Run side effects when signals change: `
|
|
13
|
+
- **State signals**: Hold values that can be directly modified: `createState()`
|
|
14
|
+
- **Store signals**: Hold objects of nested reactive properties: `createStore()`
|
|
15
|
+
- **Computed signals**: Derive memoized values from other signals: `createComputed()`
|
|
16
|
+
- **Effects**: Run side effects when signals change: `createEffect()`
|
|
17
17
|
|
|
18
18
|
## Key Features
|
|
19
19
|
|
|
@@ -28,16 +28,16 @@ Version 0.15.1
|
|
|
28
28
|
## Quick Start
|
|
29
29
|
|
|
30
30
|
```js
|
|
31
|
-
import {
|
|
31
|
+
import { createState, createComputed, createEffect } from '@zeix/cause-effect'
|
|
32
32
|
|
|
33
33
|
// 1. Create state
|
|
34
|
-
const user =
|
|
34
|
+
const user = createState({ name: 'Alice', age: 30 })
|
|
35
35
|
|
|
36
36
|
// 2. Create computed values
|
|
37
|
-
const greeting =
|
|
37
|
+
const greeting = createComputed(() => `Hello ${user.get().name}!`)
|
|
38
38
|
|
|
39
39
|
// 3. React to changes
|
|
40
|
-
|
|
40
|
+
createEffect(() => {
|
|
41
41
|
console.log(`${greeting.get()} You are ${user.get().age} years old`)
|
|
42
42
|
})
|
|
43
43
|
|
|
@@ -59,13 +59,13 @@ bun add @zeix/cause-effect
|
|
|
59
59
|
|
|
60
60
|
### State Signals
|
|
61
61
|
|
|
62
|
-
`
|
|
62
|
+
`createState()` creates a mutable signal. Every signal has a `.get()` method to access its current value. State signals also provide `.set()` to directly assign a new value and `.update()` to modify the value with a function.
|
|
63
63
|
|
|
64
64
|
```js
|
|
65
|
-
import {
|
|
65
|
+
import { createState, createEffect } from '@zeix/cause-effect'
|
|
66
66
|
|
|
67
|
-
const count =
|
|
68
|
-
|
|
67
|
+
const count = createState(42)
|
|
68
|
+
createEffect(() => {
|
|
69
69
|
console.log(count.get()) // logs '42'
|
|
70
70
|
})
|
|
71
71
|
count.set(24) // logs '24'
|
|
@@ -77,12 +77,12 @@ document.querySelector('.increment').addEventListener('click', () => {
|
|
|
77
77
|
|
|
78
78
|
### Store Signals
|
|
79
79
|
|
|
80
|
-
`
|
|
80
|
+
`createStore()` creates a mutable signal that holds an object with nested reactive properties. Each property automatically becomes its own signal with `.get()`, `.set()`, and `.update()` methods. Nested objects recursively become nested stores.
|
|
81
81
|
|
|
82
82
|
```js
|
|
83
|
-
import {
|
|
83
|
+
import { createStore, createEffect } from '@zeix/cause-effect'
|
|
84
84
|
|
|
85
|
-
const user =
|
|
85
|
+
const user = createStore({
|
|
86
86
|
name: 'Alice',
|
|
87
87
|
age: 30,
|
|
88
88
|
preferences: {
|
|
@@ -92,12 +92,12 @@ const user = store({
|
|
|
92
92
|
})
|
|
93
93
|
|
|
94
94
|
// Individual properties are reactive
|
|
95
|
-
|
|
95
|
+
createEffect(() => {
|
|
96
96
|
console.log(`${user.name.get()} is ${user.age.get()} years old`)
|
|
97
97
|
})
|
|
98
98
|
|
|
99
99
|
// Nested properties work the same way
|
|
100
|
-
|
|
100
|
+
createEffect(() => {
|
|
101
101
|
console.log(`Theme: ${user.preferences.theme.get()}`)
|
|
102
102
|
})
|
|
103
103
|
|
|
@@ -106,17 +106,24 @@ user.age.update(v => v + 1) // Logs: "Alice is 31 years old"
|
|
|
106
106
|
user.preferences.theme.set('light') // Logs: "Theme: light"
|
|
107
107
|
|
|
108
108
|
// Watch the entire store
|
|
109
|
-
|
|
109
|
+
createEffect(() => {
|
|
110
110
|
console.log('User data:', user.get()) // Triggers on any nested change
|
|
111
111
|
})
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
#### When to Use
|
|
115
|
+
|
|
116
|
+
**When to use stores vs states:**
|
|
117
|
+
|
|
118
|
+
- **Use `createStore()`** for objects with properties that you want to access and modify individually.
|
|
119
|
+
- **Use `createState()`** for primitive values (numbers, strings, booleans) or objects you access and replace entirely.
|
|
120
|
+
|
|
114
121
|
#### Dynamic Properties
|
|
115
122
|
|
|
116
123
|
Stores support dynamic property addition and removal at runtime using the `add()` and `remove()` methods:
|
|
117
124
|
|
|
118
125
|
```js
|
|
119
|
-
import {
|
|
126
|
+
import { createStore, createEffect } from '@zeix/cause-effect'
|
|
120
127
|
|
|
121
128
|
const settings = store({ autoSave: true })
|
|
122
129
|
|
|
@@ -140,61 +147,102 @@ The `add()` and `remove()` methods are optimized for performance:
|
|
|
140
147
|
- They're perfect for frequent single-property additions/removals
|
|
141
148
|
- They trigger the same events and reactivity as other store operations
|
|
142
149
|
|
|
143
|
-
####
|
|
150
|
+
#### Array-like Stores
|
|
151
|
+
|
|
152
|
+
Stores created from arrays behave like arrays with reactive properties. They support duck-typing with length property, single-parameter `add()`, and efficient sorting:
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
import { createStore, createEffect } from '@zeix/cause-effect'
|
|
156
|
+
|
|
157
|
+
const items = createStore(['banana', 'apple', 'cherry'])
|
|
158
|
+
|
|
159
|
+
// Duck-typing: behaves like an array
|
|
160
|
+
console.log(items.length) // 3
|
|
161
|
+
console.log(typeof items.length) // 'number'
|
|
162
|
+
|
|
163
|
+
// Individual items are reactive
|
|
164
|
+
createEffect(() => {
|
|
165
|
+
console.log(`First item: ${items[0].get()}`)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// Single-parameter add() appends to end
|
|
169
|
+
items.add('date') // Adds at index 3
|
|
170
|
+
console.log(items.get()) // ['banana', 'apple', 'cherry', 'date']
|
|
171
|
+
|
|
172
|
+
// Efficient sorting preserves signal references
|
|
173
|
+
items.sort() // Default: string comparison
|
|
174
|
+
console.log(items.get()) // ['apple', 'banana', 'cherry', 'date']
|
|
175
|
+
|
|
176
|
+
// Custom sorting
|
|
177
|
+
items.sort((a, b) => b.localeCompare(a)) // Reverse alphabetical
|
|
178
|
+
console.log(items.get()) // ['date', 'cherry', 'banana', 'apple']
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### Store Change Notifications
|
|
144
182
|
|
|
145
|
-
Stores emit events when properties are added, changed, or removed. You can listen to these
|
|
183
|
+
Stores emit notifications (sort of light-weight events) when properties are added, changed, or removed. You can listen to these notications using the `.on()` method:
|
|
146
184
|
|
|
147
185
|
```js
|
|
148
|
-
import {
|
|
186
|
+
import { createStore } from '@zeix/cause-effect'
|
|
149
187
|
|
|
150
|
-
const user =
|
|
188
|
+
const user = createStore({ name: 'Alice', age: 30 })
|
|
151
189
|
|
|
152
190
|
// Listen for property additions
|
|
153
|
-
user.
|
|
154
|
-
console.log('Added properties:',
|
|
191
|
+
const offAdd = user.on('add', (added) => {
|
|
192
|
+
console.log('Added properties:', added)
|
|
155
193
|
})
|
|
156
194
|
|
|
157
195
|
// Listen for property changes
|
|
158
|
-
user.
|
|
159
|
-
console.log('Changed properties:',
|
|
196
|
+
const offChange = user.on('change', (changed) => {
|
|
197
|
+
console.log('Changed properties:', changed)
|
|
160
198
|
})
|
|
161
199
|
|
|
162
200
|
// Listen for property removals
|
|
163
|
-
user.
|
|
164
|
-
console.log('Removed properties:',
|
|
201
|
+
const offRemove = user.on('remove', (removed) => {
|
|
202
|
+
console.log('Removed properties:', removed)
|
|
165
203
|
})
|
|
166
204
|
|
|
167
|
-
// These will trigger the respective
|
|
205
|
+
// These will trigger the respective notifications:
|
|
168
206
|
user.add('email', 'alice@example.com') // Logs: "Added properties: { email: 'alice@example.com' }"
|
|
169
207
|
user.age.set(31) // Logs: "Changed properties: { age: 31 }"
|
|
170
208
|
user.remove('email') // Logs: "Removed properties: { email: UNSET }"
|
|
209
|
+
|
|
210
|
+
// Listen for sort notifications (useful for UI animations)
|
|
211
|
+
const items = createStore(['banana', 'apple', 'cherry'])
|
|
212
|
+
items.sort((a, b) => b.localeCompare(a)) // Reverse alphabetical
|
|
213
|
+
const offSort = items.on('sort', (newOrder) => {
|
|
214
|
+
console.log('Items reordered:', newOrder) // ['2', '1', '0']
|
|
215
|
+
})
|
|
171
216
|
```
|
|
172
217
|
|
|
173
|
-
|
|
218
|
+
Notifications are also fired when using `set()` or `update()` methods on the entire store:
|
|
174
219
|
|
|
175
220
|
```js
|
|
176
|
-
// This will fire multiple
|
|
221
|
+
// This will fire multiple notifications based on what changed
|
|
177
222
|
user.update(u => ({ ...u, name: 'Bob', city: 'New York' }))
|
|
178
223
|
// Logs: "Changed properties: { name: 'Bob' }"
|
|
179
224
|
// Logs: "Added properties: { city: 'New York' }"
|
|
180
225
|
```
|
|
181
226
|
|
|
182
|
-
|
|
183
|
-
- **Use `store()`** for objects with reactive properties that you want to access individually
|
|
184
|
-
- **Use `state()`** for primitive values or objects you replace entirely
|
|
227
|
+
To stop listening to notifications, call the returned cleanup function:
|
|
185
228
|
|
|
186
|
-
|
|
229
|
+
```js
|
|
230
|
+
offAdd() // Stops listening to add notifications
|
|
231
|
+
offChange() // Stops listening to change notifications
|
|
232
|
+
offRemove() // Stops listening to remove notifications
|
|
233
|
+
offSort() // Stops listening to sort notifications
|
|
234
|
+
```
|
|
187
235
|
|
|
188
|
-
|
|
236
|
+
### Computed Signals
|
|
189
237
|
|
|
190
|
-
`
|
|
238
|
+
`createComputed()` creates a memoized read-only signal that automatically tracks dependencies and updates only when those dependencies change.
|
|
191
239
|
|
|
192
240
|
```js
|
|
193
|
-
import {
|
|
241
|
+
import { createState, createComputed, createEffect } from '@zeix/cause-effect'
|
|
194
242
|
|
|
195
|
-
const count =
|
|
196
|
-
const isEven =
|
|
197
|
-
|
|
243
|
+
const count = createState(42)
|
|
244
|
+
const isEven = createComputed(() => !(count.get() % 2))
|
|
245
|
+
createEffect(() => console.log(isEven.get())) // logs 'true'
|
|
198
246
|
count.set(24) // logs nothing because 24 is also an even number
|
|
199
247
|
document.querySelector('button.increment').addEventListener('click', () => {
|
|
200
248
|
count.update(v => ++v)
|
|
@@ -202,7 +250,7 @@ document.querySelector('button.increment').addEventListener('click', () => {
|
|
|
202
250
|
// Click on button logs 'false', 'true', and so on
|
|
203
251
|
```
|
|
204
252
|
|
|
205
|
-
#### When to Use
|
|
253
|
+
#### When to Use
|
|
206
254
|
|
|
207
255
|
**Performance tip**: For simple derivations, plain functions often outperform computed signals:
|
|
208
256
|
|
|
@@ -213,16 +261,53 @@ const isEven = () => !(count.get() % 2)
|
|
|
213
261
|
|
|
214
262
|
**When to use which approach:**
|
|
215
263
|
|
|
216
|
-
- **Use functions when**: The calculation is simple, inexpensive, or called infrequently
|
|
217
|
-
- **Use
|
|
264
|
+
- **Use functions when**: The calculation is simple, inexpensive, or called infrequently.
|
|
265
|
+
- **Use createComputed() when**:
|
|
218
266
|
- The calculation is expensive
|
|
219
267
|
- You need to share the result between multiple consumers
|
|
220
268
|
- You're working with asynchronous operations
|
|
221
269
|
- You need to track specific error states
|
|
270
|
+
|
|
271
|
+
#### Reducer-like Capabilities
|
|
272
|
+
|
|
273
|
+
`createComputed()` supports reducer-like patterns by accepting an initial value and providing access to the previous value in the callback:
|
|
274
|
+
|
|
275
|
+
```js
|
|
276
|
+
import { createState, createComputed, createEffect } from '@zeix/cause-effect'
|
|
277
|
+
|
|
278
|
+
const actions = createState('increment')
|
|
279
|
+
const counter = createComputed((prev, abort) => {
|
|
280
|
+
const action = actions.get()
|
|
281
|
+
switch (action) {
|
|
282
|
+
case 'increment':
|
|
283
|
+
return prev + 1
|
|
284
|
+
case 'decrement':
|
|
285
|
+
return prev - 1
|
|
286
|
+
case 'reset':
|
|
287
|
+
return 0
|
|
288
|
+
default:
|
|
289
|
+
return prev
|
|
290
|
+
}
|
|
291
|
+
}, 0) // Initial value of 0
|
|
292
|
+
|
|
293
|
+
createEffect(() => console.log('Counter:', counter.get()))
|
|
294
|
+
|
|
295
|
+
// Dispatch actions
|
|
296
|
+
actions.set('increment') // Counter: 1
|
|
297
|
+
actions.set('increment') // Counter: 2
|
|
298
|
+
actions.set('decrement') // Counter: 1
|
|
299
|
+
actions.set('reset') // Counter: 0
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
This pattern is particularly useful for:
|
|
303
|
+
- State machines with transitions based on current state
|
|
304
|
+
- Accumulating values over time
|
|
305
|
+
- Complex state updates that depend on previous state
|
|
306
|
+
- Building reactive reducers similar to Redux patterns
|
|
222
307
|
|
|
223
308
|
#### Asynchronous Computations with Automatic Cancellation
|
|
224
309
|
|
|
225
|
-
`
|
|
310
|
+
`createComputed()` seamlessly handles asynchronous operations with built-in cancellation support. When used with an async function, it:
|
|
226
311
|
|
|
227
312
|
1. Provides an `abort` signal parameter you can pass to fetch or other cancelable APIs
|
|
228
313
|
2. Automatically cancels pending operations when dependencies change
|
|
@@ -230,10 +315,10 @@ const isEven = () => !(count.get() % 2)
|
|
|
230
315
|
4. Properly handles errors from failed requests
|
|
231
316
|
|
|
232
317
|
```js
|
|
233
|
-
import {
|
|
318
|
+
import { createState, createComputed, createEffect, resolve, match } from '@zeix/cause-effect'
|
|
234
319
|
|
|
235
|
-
const id =
|
|
236
|
-
const data =
|
|
320
|
+
const id = createState(42)
|
|
321
|
+
const data = createComputed(async (_, abort) => {
|
|
237
322
|
// The abort signal is automatically managed by the computed signal
|
|
238
323
|
const response = await fetch(`/api/entries/${id.get()}`, { signal: abort })
|
|
239
324
|
if (!response.ok) throw new Error(`Failed to fetch data: ${response.statusText}`)
|
|
@@ -241,7 +326,7 @@ const data = computed(async abort => {
|
|
|
241
326
|
})
|
|
242
327
|
|
|
243
328
|
// Handle all possible states using resolve and match helpers
|
|
244
|
-
|
|
329
|
+
createEffect(() => {
|
|
245
330
|
match(resolve({ data }), {
|
|
246
331
|
ok: ({ data: json }) => console.log('Data loaded:', json),
|
|
247
332
|
nil: () => console.log('Loading...'),
|
|
@@ -255,19 +340,19 @@ document.querySelector('button.next').addEventListener('click', () => {
|
|
|
255
340
|
})
|
|
256
341
|
```
|
|
257
342
|
|
|
258
|
-
**Note**: Always use `
|
|
343
|
+
**Note**: Always use `createComputed()` (not plain functions) for async operations to benefit from automatic cancellation and memoization.
|
|
259
344
|
|
|
260
345
|
## Effects and Error Handling
|
|
261
346
|
|
|
262
|
-
The `
|
|
347
|
+
The `createEffect()` function supports both synchronous and asynchronous callbacks:
|
|
263
348
|
|
|
264
349
|
### Synchronous Effects
|
|
265
350
|
|
|
266
351
|
```js
|
|
267
|
-
import {
|
|
352
|
+
import { createState, createEffect } from '@zeix/cause-effect'
|
|
268
353
|
|
|
269
|
-
const count =
|
|
270
|
-
|
|
354
|
+
const count = createState(42)
|
|
355
|
+
createEffect(() => {
|
|
271
356
|
console.log('Count changed:', count.get())
|
|
272
357
|
})
|
|
273
358
|
```
|
|
@@ -277,10 +362,10 @@ effect(() => {
|
|
|
277
362
|
Async effect callbacks receive an `AbortSignal` parameter that automatically cancels when the effect re-runs or is cleaned up:
|
|
278
363
|
|
|
279
364
|
```js
|
|
280
|
-
import {
|
|
365
|
+
import { createState, createEffect } from '@zeix/cause-effect'
|
|
281
366
|
|
|
282
|
-
const userId =
|
|
283
|
-
|
|
367
|
+
const userId = createState(1)
|
|
368
|
+
createEffect(async (abort) => {
|
|
284
369
|
try {
|
|
285
370
|
const response = await fetch(`/api/users/${userId.get()}`, { signal: abort })
|
|
286
371
|
const user = await response.json()
|
|
@@ -298,16 +383,16 @@ effect(async (abort) => {
|
|
|
298
383
|
For more sophisticated error handling, use the `resolve()` and `match()` helper functions:
|
|
299
384
|
|
|
300
385
|
```js
|
|
301
|
-
import {
|
|
386
|
+
import { createState, createEffect, resolve, match } from '@zeix/cause-effect'
|
|
302
387
|
|
|
303
|
-
const userId =
|
|
304
|
-
const userData =
|
|
388
|
+
const userId = createState(1)
|
|
389
|
+
const userData = createEffect(async (abort) => {
|
|
305
390
|
const response = await fetch(`/api/users/${userId.get()}`, { signal: abort })
|
|
306
391
|
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
307
392
|
return response.json()
|
|
308
393
|
})
|
|
309
394
|
|
|
310
|
-
|
|
395
|
+
createEffect(() => {
|
|
311
396
|
match(resolve({ userData }), {
|
|
312
397
|
ok: ({ userData: user }) => console.log('User loaded:', user),
|
|
313
398
|
nil: () => console.log('Loading user...'),
|
|
@@ -318,90 +403,6 @@ effect(() => {
|
|
|
318
403
|
|
|
319
404
|
The `resolve()` function extracts values from signals and returns a discriminated union result, while `match()` provides pattern matching for handling different states declaratively.
|
|
320
405
|
|
|
321
|
-
## DOM Updates
|
|
322
|
-
|
|
323
|
-
The `enqueue()` function allows you to schedule DOM updates to be executed on the next animation frame. It returns a `Promise`, which makes it easy to track when updates are applied or handle errors.
|
|
324
|
-
|
|
325
|
-
```js
|
|
326
|
-
import { enqueue } from '@zeix/cause-effect'
|
|
327
|
-
|
|
328
|
-
// Schedule a DOM update
|
|
329
|
-
enqueue(() => {
|
|
330
|
-
document.getElementById('myElement').textContent = 'Updated content'
|
|
331
|
-
})
|
|
332
|
-
.then(() => console.log('Update applied successfully'))
|
|
333
|
-
.catch(error => console.error('Update failed:', error))
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
### Deduplication with Symbols
|
|
337
|
-
|
|
338
|
-
A powerful feature of `enqueue()` is deduplication, which ensures that only the most recent update for a specific operation is applied when multiple updates occur within a single animation frame. This is particularly useful for high-frequency events like typing, dragging, or scrolling.
|
|
339
|
-
|
|
340
|
-
Deduplication is controlled using JavaScript Symbols:
|
|
341
|
-
|
|
342
|
-
```js
|
|
343
|
-
import { state, effect, enqueue } from '@zeix/cause-effect'
|
|
344
|
-
|
|
345
|
-
// Define a signal and update it in an event handler
|
|
346
|
-
const name = state('')
|
|
347
|
-
document.querySelector('input[name="name"]').addEventListener('input', e => {
|
|
348
|
-
name.set(e.target.value) // Triggers an update on every keystroke
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
// Define an effect to react to signal changes
|
|
352
|
-
effect(text => {
|
|
353
|
-
// Create a Symbol for a specific update operation
|
|
354
|
-
const NAME_UPDATE = Symbol('name-update')
|
|
355
|
-
const text = name.get()
|
|
356
|
-
const nameSpan = document.querySelector('.greeting .name')
|
|
357
|
-
enqueue(() => {
|
|
358
|
-
nameSpan.textContent = text
|
|
359
|
-
return text
|
|
360
|
-
}, NAME_UPDATE) // Using the Symbol for deduplication
|
|
361
|
-
.then(result => console.log(`Name was updated to ${result}`))
|
|
362
|
-
.catch(error => console.error('Failed to update name:', error))
|
|
363
|
-
})
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
In this example, as the user types "Jane" quickly, the intermediate values ('J', 'Ja', 'Jan') are deduplicated, and only the final value 'Jane' is applied to the DOM. Only the Promise for the final update is resolved.
|
|
367
|
-
|
|
368
|
-
### How Deduplication Works
|
|
369
|
-
|
|
370
|
-
When multiple `enqueue` calls use the same Symbol before the next animation frame:
|
|
371
|
-
|
|
372
|
-
1. Only the last call will be executed
|
|
373
|
-
2. Previous calls are superseded
|
|
374
|
-
3. Only the Promise of the last call will be resolved
|
|
375
|
-
|
|
376
|
-
This "last-write-wins" behavior optimizes DOM updates and prevents unnecessary work when many updates happen rapidly.
|
|
377
|
-
|
|
378
|
-
### Optional Deduplication
|
|
379
|
-
|
|
380
|
-
The deduplication Symbol is optional. When not provided, a unique Symbol is created automatically, ensuring the update is always executed:
|
|
381
|
-
|
|
382
|
-
```js
|
|
383
|
-
// No deduplication - always executed
|
|
384
|
-
enqueue(() => document.title = 'New Page Title')
|
|
385
|
-
|
|
386
|
-
// Create symbols for different types of updates
|
|
387
|
-
const COLOR_UPDATE = Symbol('color-update')
|
|
388
|
-
const SIZE_UPDATE = Symbol('size-update')
|
|
389
|
-
|
|
390
|
-
// These won't interfere with each other (different symbols)
|
|
391
|
-
enqueue(() => element.style.color = 'red', COLOR_UPDATE)
|
|
392
|
-
enqueue(() => element.style.fontSize = '16px', SIZE_UPDATE)
|
|
393
|
-
|
|
394
|
-
// This will replace the previous color update (same symbol)
|
|
395
|
-
enqueue(() => element.style.color = 'blue', COLOR_UPDATE)
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
Using Symbols for deduplication provides:
|
|
399
|
-
|
|
400
|
-
- Clear semantic meaning for update operations
|
|
401
|
-
- Type safety in TypeScript
|
|
402
|
-
- Simple mechanism to control which updates should overwrite each other
|
|
403
|
-
- Flexibility to run every update when needed
|
|
404
|
-
|
|
405
406
|
## Advanced Usage
|
|
406
407
|
|
|
407
408
|
### Batching Updates
|
|
@@ -409,13 +410,20 @@ Using Symbols for deduplication provides:
|
|
|
409
410
|
Use `batch()` to group multiple signal updates, ensuring effects run only once after all changes are applied:
|
|
410
411
|
|
|
411
412
|
```js
|
|
412
|
-
import {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
413
|
+
import {
|
|
414
|
+
createState,
|
|
415
|
+
createComputed,
|
|
416
|
+
createEffect,
|
|
417
|
+
batch,
|
|
418
|
+
resolve,
|
|
419
|
+
match
|
|
420
|
+
} from '@zeix/cause-effect'
|
|
421
|
+
|
|
422
|
+
// State: define an Array<State<number>>
|
|
423
|
+
const signals = [createState(2), createState(3), createState(5)]
|
|
416
424
|
|
|
417
425
|
// Compute the sum of all signals
|
|
418
|
-
const sum =
|
|
426
|
+
const sum = createComputed(() => {
|
|
419
427
|
const v = signals.reduce((total, signal) => total + signal.get(), 0)
|
|
420
428
|
// Validate the result
|
|
421
429
|
if (!Number.isFinite(v)) throw new Error('Invalid value')
|
|
@@ -423,7 +431,7 @@ const sum = computed(() => {
|
|
|
423
431
|
})
|
|
424
432
|
|
|
425
433
|
// Effect: handle the result with error handling
|
|
426
|
-
|
|
434
|
+
createEffect(() => {
|
|
427
435
|
match(resolve({ sum }), {
|
|
428
436
|
ok: ({ sum: v }) => console.log('Sum:', v),
|
|
429
437
|
err: errors => console.error('Error:', errors[0])
|
|
@@ -450,11 +458,11 @@ signals[0].set(NaN)
|
|
|
450
458
|
Effects return a cleanup function. When executed, it will unsubscribe from signals and run cleanup functions returned by effect callbacks, for example to remove event listeners.
|
|
451
459
|
|
|
452
460
|
```js
|
|
453
|
-
import {
|
|
461
|
+
import { createState, createComputed, createEffect } from '@zeix/cause-effect'
|
|
454
462
|
|
|
455
|
-
const user =
|
|
463
|
+
const user = createState({ name: 'Alice', age: 30 })
|
|
456
464
|
const greeting = () => `Hello ${user.get().name}!`
|
|
457
|
-
const cleanup =
|
|
465
|
+
const cleanup = createEffect(() => {
|
|
458
466
|
console.log(`${greeting()} You are ${user.get().age} years old`)
|
|
459
467
|
return () => console.log('Cleanup') // Cleanup function
|
|
460
468
|
})
|
|
@@ -472,10 +480,10 @@ user.set({ name: 'Bob', age: 28 }) // Won't trigger the effect anymore
|
|
|
472
480
|
The `resolve()` function extracts values from multiple signals and returns a discriminated union result:
|
|
473
481
|
|
|
474
482
|
```js
|
|
475
|
-
import {
|
|
483
|
+
import { createState, createComputed, resolve } from '@zeix/cause-effect'
|
|
476
484
|
|
|
477
|
-
const name =
|
|
478
|
-
const age =
|
|
485
|
+
const name = createState('Alice')
|
|
486
|
+
const age = createComputed(() => 30)
|
|
479
487
|
const result = resolve({ name, age })
|
|
480
488
|
|
|
481
489
|
if (result.ok) {
|