@zeix/cause-effect 0.15.2 → 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 +136 -166
- package/eslint.config.js +1 -1
- package/index.dev.js +125 -129
- package/index.js +1 -1
- package/index.ts +22 -22
- package/package.json +1 -1
- package/src/computed.ts +40 -29
- package/src/effect.ts +15 -12
- package/src/errors.ts +8 -0
- package/src/signal.ts +6 -6
- package/src/state.ts +27 -20
- package/src/store.ts +99 -121
- package/src/system.ts +122 -0
- package/src/util.ts +1 -6
- package/test/batch.test.ts +18 -11
- package/test/benchmark.test.ts +4 -4
- package/test/computed.test.ts +507 -71
- package/test/effect.test.ts +60 -60
- package/test/match.test.ts +25 -25
- package/test/resolve.test.ts +16 -16
- package/test/signal.test.ts +7 -7
- package/test/state.test.ts +212 -25
- package/test/store.test.ts +476 -183
- package/test/util/dependency-graph.ts +1 -1
- package/types/index.d.ts +8 -8
- package/types/src/collection.d.ts +26 -0
- package/types/src/computed.d.ts +9 -9
- package/types/src/effect.d.ts +3 -3
- package/types/src/errors.d.ts +4 -1
- package/types/src/state.d.ts +5 -5
- package/types/src/store.d.ts +27 -41
- package/types/src/system.d.ts +44 -0
- package/types/src/util.d.ts +1 -2
- package/src/scheduler.ts +0 -172
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.2
|
|
|
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.2
|
|
|
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
|
|
|
@@ -145,16 +152,16 @@ The `add()` and `remove()` methods are optimized for performance:
|
|
|
145
152
|
Stores created from arrays behave like arrays with reactive properties. They support duck-typing with length property, single-parameter `add()`, and efficient sorting:
|
|
146
153
|
|
|
147
154
|
```js
|
|
148
|
-
import {
|
|
155
|
+
import { createStore, createEffect } from '@zeix/cause-effect'
|
|
149
156
|
|
|
150
|
-
const items =
|
|
157
|
+
const items = createStore(['banana', 'apple', 'cherry'])
|
|
151
158
|
|
|
152
159
|
// Duck-typing: behaves like an array
|
|
153
160
|
console.log(items.length) // 3
|
|
154
161
|
console.log(typeof items.length) // 'number'
|
|
155
162
|
|
|
156
163
|
// Individual items are reactive
|
|
157
|
-
|
|
164
|
+
createEffect(() => {
|
|
158
165
|
console.log(`First item: ${items[0].get()}`)
|
|
159
166
|
})
|
|
160
167
|
|
|
@@ -171,68 +178,71 @@ items.sort((a, b) => b.localeCompare(a)) // Reverse alphabetical
|
|
|
171
178
|
console.log(items.get()) // ['date', 'cherry', 'banana', 'apple']
|
|
172
179
|
```
|
|
173
180
|
|
|
174
|
-
#### Store
|
|
181
|
+
#### Store Change Notifications
|
|
175
182
|
|
|
176
|
-
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:
|
|
177
184
|
|
|
178
185
|
```js
|
|
179
|
-
import {
|
|
186
|
+
import { createStore } from '@zeix/cause-effect'
|
|
180
187
|
|
|
181
|
-
const user =
|
|
188
|
+
const user = createStore({ name: 'Alice', age: 30 })
|
|
182
189
|
|
|
183
190
|
// Listen for property additions
|
|
184
|
-
user.
|
|
185
|
-
console.log('Added properties:',
|
|
191
|
+
const offAdd = user.on('add', (added) => {
|
|
192
|
+
console.log('Added properties:', added)
|
|
186
193
|
})
|
|
187
194
|
|
|
188
195
|
// Listen for property changes
|
|
189
|
-
user.
|
|
190
|
-
console.log('Changed properties:',
|
|
196
|
+
const offChange = user.on('change', (changed) => {
|
|
197
|
+
console.log('Changed properties:', changed)
|
|
191
198
|
})
|
|
192
199
|
|
|
193
200
|
// Listen for property removals
|
|
194
|
-
user.
|
|
195
|
-
console.log('Removed properties:',
|
|
201
|
+
const offRemove = user.on('remove', (removed) => {
|
|
202
|
+
console.log('Removed properties:', removed)
|
|
196
203
|
})
|
|
197
204
|
|
|
198
|
-
// These will trigger the respective
|
|
205
|
+
// These will trigger the respective notifications:
|
|
199
206
|
user.add('email', 'alice@example.com') // Logs: "Added properties: { email: 'alice@example.com' }"
|
|
200
207
|
user.age.set(31) // Logs: "Changed properties: { age: 31 }"
|
|
201
208
|
user.remove('email') // Logs: "Removed properties: { email: UNSET }"
|
|
202
209
|
|
|
203
|
-
// Listen for sort
|
|
204
|
-
const items =
|
|
210
|
+
// Listen for sort notifications (useful for UI animations)
|
|
211
|
+
const items = createStore(['banana', 'apple', 'cherry'])
|
|
205
212
|
items.sort((a, b) => b.localeCompare(a)) // Reverse alphabetical
|
|
206
|
-
items.
|
|
207
|
-
console.log('Items reordered:',
|
|
213
|
+
const offSort = items.on('sort', (newOrder) => {
|
|
214
|
+
console.log('Items reordered:', newOrder) // ['2', '1', '0']
|
|
208
215
|
})
|
|
209
216
|
```
|
|
210
217
|
|
|
211
|
-
|
|
218
|
+
Notifications are also fired when using `set()` or `update()` methods on the entire store:
|
|
212
219
|
|
|
213
220
|
```js
|
|
214
|
-
// This will fire multiple
|
|
221
|
+
// This will fire multiple notifications based on what changed
|
|
215
222
|
user.update(u => ({ ...u, name: 'Bob', city: 'New York' }))
|
|
216
223
|
// Logs: "Changed properties: { name: 'Bob' }"
|
|
217
224
|
// Logs: "Added properties: { city: 'New York' }"
|
|
218
225
|
```
|
|
219
226
|
|
|
220
|
-
|
|
221
|
-
- **Use `store()`** for objects with reactive properties that you want to access individually
|
|
222
|
-
- **Use `state()`** for primitive values or objects you replace entirely
|
|
227
|
+
To stop listening to notifications, call the returned cleanup function:
|
|
223
228
|
|
|
224
|
-
|
|
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
|
+
```
|
|
225
235
|
|
|
226
|
-
|
|
236
|
+
### Computed Signals
|
|
227
237
|
|
|
228
|
-
`
|
|
238
|
+
`createComputed()` creates a memoized read-only signal that automatically tracks dependencies and updates only when those dependencies change.
|
|
229
239
|
|
|
230
240
|
```js
|
|
231
|
-
import {
|
|
241
|
+
import { createState, createComputed, createEffect } from '@zeix/cause-effect'
|
|
232
242
|
|
|
233
|
-
const count =
|
|
234
|
-
const isEven =
|
|
235
|
-
|
|
243
|
+
const count = createState(42)
|
|
244
|
+
const isEven = createComputed(() => !(count.get() % 2))
|
|
245
|
+
createEffect(() => console.log(isEven.get())) // logs 'true'
|
|
236
246
|
count.set(24) // logs nothing because 24 is also an even number
|
|
237
247
|
document.querySelector('button.increment').addEventListener('click', () => {
|
|
238
248
|
count.update(v => ++v)
|
|
@@ -240,7 +250,7 @@ document.querySelector('button.increment').addEventListener('click', () => {
|
|
|
240
250
|
// Click on button logs 'false', 'true', and so on
|
|
241
251
|
```
|
|
242
252
|
|
|
243
|
-
#### When to Use
|
|
253
|
+
#### When to Use
|
|
244
254
|
|
|
245
255
|
**Performance tip**: For simple derivations, plain functions often outperform computed signals:
|
|
246
256
|
|
|
@@ -251,16 +261,53 @@ const isEven = () => !(count.get() % 2)
|
|
|
251
261
|
|
|
252
262
|
**When to use which approach:**
|
|
253
263
|
|
|
254
|
-
- **Use functions when**: The calculation is simple, inexpensive, or called infrequently
|
|
255
|
-
- **Use
|
|
264
|
+
- **Use functions when**: The calculation is simple, inexpensive, or called infrequently.
|
|
265
|
+
- **Use createComputed() when**:
|
|
256
266
|
- The calculation is expensive
|
|
257
267
|
- You need to share the result between multiple consumers
|
|
258
268
|
- You're working with asynchronous operations
|
|
259
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
|
|
260
307
|
|
|
261
308
|
#### Asynchronous Computations with Automatic Cancellation
|
|
262
309
|
|
|
263
|
-
`
|
|
310
|
+
`createComputed()` seamlessly handles asynchronous operations with built-in cancellation support. When used with an async function, it:
|
|
264
311
|
|
|
265
312
|
1. Provides an `abort` signal parameter you can pass to fetch or other cancelable APIs
|
|
266
313
|
2. Automatically cancels pending operations when dependencies change
|
|
@@ -268,10 +315,10 @@ const isEven = () => !(count.get() % 2)
|
|
|
268
315
|
4. Properly handles errors from failed requests
|
|
269
316
|
|
|
270
317
|
```js
|
|
271
|
-
import {
|
|
318
|
+
import { createState, createComputed, createEffect, resolve, match } from '@zeix/cause-effect'
|
|
272
319
|
|
|
273
|
-
const id =
|
|
274
|
-
const data =
|
|
320
|
+
const id = createState(42)
|
|
321
|
+
const data = createComputed(async (_, abort) => {
|
|
275
322
|
// The abort signal is automatically managed by the computed signal
|
|
276
323
|
const response = await fetch(`/api/entries/${id.get()}`, { signal: abort })
|
|
277
324
|
if (!response.ok) throw new Error(`Failed to fetch data: ${response.statusText}`)
|
|
@@ -279,7 +326,7 @@ const data = computed(async abort => {
|
|
|
279
326
|
})
|
|
280
327
|
|
|
281
328
|
// Handle all possible states using resolve and match helpers
|
|
282
|
-
|
|
329
|
+
createEffect(() => {
|
|
283
330
|
match(resolve({ data }), {
|
|
284
331
|
ok: ({ data: json }) => console.log('Data loaded:', json),
|
|
285
332
|
nil: () => console.log('Loading...'),
|
|
@@ -293,19 +340,19 @@ document.querySelector('button.next').addEventListener('click', () => {
|
|
|
293
340
|
})
|
|
294
341
|
```
|
|
295
342
|
|
|
296
|
-
**Note**: Always use `
|
|
343
|
+
**Note**: Always use `createComputed()` (not plain functions) for async operations to benefit from automatic cancellation and memoization.
|
|
297
344
|
|
|
298
345
|
## Effects and Error Handling
|
|
299
346
|
|
|
300
|
-
The `
|
|
347
|
+
The `createEffect()` function supports both synchronous and asynchronous callbacks:
|
|
301
348
|
|
|
302
349
|
### Synchronous Effects
|
|
303
350
|
|
|
304
351
|
```js
|
|
305
|
-
import {
|
|
352
|
+
import { createState, createEffect } from '@zeix/cause-effect'
|
|
306
353
|
|
|
307
|
-
const count =
|
|
308
|
-
|
|
354
|
+
const count = createState(42)
|
|
355
|
+
createEffect(() => {
|
|
309
356
|
console.log('Count changed:', count.get())
|
|
310
357
|
})
|
|
311
358
|
```
|
|
@@ -315,10 +362,10 @@ effect(() => {
|
|
|
315
362
|
Async effect callbacks receive an `AbortSignal` parameter that automatically cancels when the effect re-runs or is cleaned up:
|
|
316
363
|
|
|
317
364
|
```js
|
|
318
|
-
import {
|
|
365
|
+
import { createState, createEffect } from '@zeix/cause-effect'
|
|
319
366
|
|
|
320
|
-
const userId =
|
|
321
|
-
|
|
367
|
+
const userId = createState(1)
|
|
368
|
+
createEffect(async (abort) => {
|
|
322
369
|
try {
|
|
323
370
|
const response = await fetch(`/api/users/${userId.get()}`, { signal: abort })
|
|
324
371
|
const user = await response.json()
|
|
@@ -336,16 +383,16 @@ effect(async (abort) => {
|
|
|
336
383
|
For more sophisticated error handling, use the `resolve()` and `match()` helper functions:
|
|
337
384
|
|
|
338
385
|
```js
|
|
339
|
-
import {
|
|
386
|
+
import { createState, createEffect, resolve, match } from '@zeix/cause-effect'
|
|
340
387
|
|
|
341
|
-
const userId =
|
|
342
|
-
const userData =
|
|
388
|
+
const userId = createState(1)
|
|
389
|
+
const userData = createEffect(async (abort) => {
|
|
343
390
|
const response = await fetch(`/api/users/${userId.get()}`, { signal: abort })
|
|
344
391
|
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
345
392
|
return response.json()
|
|
346
393
|
})
|
|
347
394
|
|
|
348
|
-
|
|
395
|
+
createEffect(() => {
|
|
349
396
|
match(resolve({ userData }), {
|
|
350
397
|
ok: ({ userData: user }) => console.log('User loaded:', user),
|
|
351
398
|
nil: () => console.log('Loading user...'),
|
|
@@ -356,90 +403,6 @@ effect(() => {
|
|
|
356
403
|
|
|
357
404
|
The `resolve()` function extracts values from signals and returns a discriminated union result, while `match()` provides pattern matching for handling different states declaratively.
|
|
358
405
|
|
|
359
|
-
## DOM Updates
|
|
360
|
-
|
|
361
|
-
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.
|
|
362
|
-
|
|
363
|
-
```js
|
|
364
|
-
import { enqueue } from '@zeix/cause-effect'
|
|
365
|
-
|
|
366
|
-
// Schedule a DOM update
|
|
367
|
-
enqueue(() => {
|
|
368
|
-
document.getElementById('myElement').textContent = 'Updated content'
|
|
369
|
-
})
|
|
370
|
-
.then(() => console.log('Update applied successfully'))
|
|
371
|
-
.catch(error => console.error('Update failed:', error))
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
### Deduplication with Symbols
|
|
375
|
-
|
|
376
|
-
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.
|
|
377
|
-
|
|
378
|
-
Deduplication is controlled using JavaScript Symbols:
|
|
379
|
-
|
|
380
|
-
```js
|
|
381
|
-
import { state, effect, enqueue } from '@zeix/cause-effect'
|
|
382
|
-
|
|
383
|
-
// Define a signal and update it in an event handler
|
|
384
|
-
const name = state('')
|
|
385
|
-
document.querySelector('input[name="name"]').addEventListener('input', e => {
|
|
386
|
-
name.set(e.target.value) // Triggers an update on every keystroke
|
|
387
|
-
})
|
|
388
|
-
|
|
389
|
-
// Define an effect to react to signal changes
|
|
390
|
-
effect(text => {
|
|
391
|
-
// Create a Symbol for a specific update operation
|
|
392
|
-
const NAME_UPDATE = Symbol('name-update')
|
|
393
|
-
const text = name.get()
|
|
394
|
-
const nameSpan = document.querySelector('.greeting .name')
|
|
395
|
-
enqueue(() => {
|
|
396
|
-
nameSpan.textContent = text
|
|
397
|
-
return text
|
|
398
|
-
}, NAME_UPDATE) // Using the Symbol for deduplication
|
|
399
|
-
.then(result => console.log(`Name was updated to ${result}`))
|
|
400
|
-
.catch(error => console.error('Failed to update name:', error))
|
|
401
|
-
})
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
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.
|
|
405
|
-
|
|
406
|
-
### How Deduplication Works
|
|
407
|
-
|
|
408
|
-
When multiple `enqueue` calls use the same Symbol before the next animation frame:
|
|
409
|
-
|
|
410
|
-
1. Only the last call will be executed
|
|
411
|
-
2. Previous calls are superseded
|
|
412
|
-
3. Only the Promise of the last call will be resolved
|
|
413
|
-
|
|
414
|
-
This "last-write-wins" behavior optimizes DOM updates and prevents unnecessary work when many updates happen rapidly.
|
|
415
|
-
|
|
416
|
-
### Optional Deduplication
|
|
417
|
-
|
|
418
|
-
The deduplication Symbol is optional. When not provided, a unique Symbol is created automatically, ensuring the update is always executed:
|
|
419
|
-
|
|
420
|
-
```js
|
|
421
|
-
// No deduplication - always executed
|
|
422
|
-
enqueue(() => document.title = 'New Page Title')
|
|
423
|
-
|
|
424
|
-
// Create symbols for different types of updates
|
|
425
|
-
const COLOR_UPDATE = Symbol('color-update')
|
|
426
|
-
const SIZE_UPDATE = Symbol('size-update')
|
|
427
|
-
|
|
428
|
-
// These won't interfere with each other (different symbols)
|
|
429
|
-
enqueue(() => element.style.color = 'red', COLOR_UPDATE)
|
|
430
|
-
enqueue(() => element.style.fontSize = '16px', SIZE_UPDATE)
|
|
431
|
-
|
|
432
|
-
// This will replace the previous color update (same symbol)
|
|
433
|
-
enqueue(() => element.style.color = 'blue', COLOR_UPDATE)
|
|
434
|
-
```
|
|
435
|
-
|
|
436
|
-
Using Symbols for deduplication provides:
|
|
437
|
-
|
|
438
|
-
- Clear semantic meaning for update operations
|
|
439
|
-
- Type safety in TypeScript
|
|
440
|
-
- Simple mechanism to control which updates should overwrite each other
|
|
441
|
-
- Flexibility to run every update when needed
|
|
442
|
-
|
|
443
406
|
## Advanced Usage
|
|
444
407
|
|
|
445
408
|
### Batching Updates
|
|
@@ -447,13 +410,20 @@ Using Symbols for deduplication provides:
|
|
|
447
410
|
Use `batch()` to group multiple signal updates, ensuring effects run only once after all changes are applied:
|
|
448
411
|
|
|
449
412
|
```js
|
|
450
|
-
import {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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)]
|
|
454
424
|
|
|
455
425
|
// Compute the sum of all signals
|
|
456
|
-
const sum =
|
|
426
|
+
const sum = createComputed(() => {
|
|
457
427
|
const v = signals.reduce((total, signal) => total + signal.get(), 0)
|
|
458
428
|
// Validate the result
|
|
459
429
|
if (!Number.isFinite(v)) throw new Error('Invalid value')
|
|
@@ -461,7 +431,7 @@ const sum = computed(() => {
|
|
|
461
431
|
})
|
|
462
432
|
|
|
463
433
|
// Effect: handle the result with error handling
|
|
464
|
-
|
|
434
|
+
createEffect(() => {
|
|
465
435
|
match(resolve({ sum }), {
|
|
466
436
|
ok: ({ sum: v }) => console.log('Sum:', v),
|
|
467
437
|
err: errors => console.error('Error:', errors[0])
|
|
@@ -488,11 +458,11 @@ signals[0].set(NaN)
|
|
|
488
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.
|
|
489
459
|
|
|
490
460
|
```js
|
|
491
|
-
import {
|
|
461
|
+
import { createState, createComputed, createEffect } from '@zeix/cause-effect'
|
|
492
462
|
|
|
493
|
-
const user =
|
|
463
|
+
const user = createState({ name: 'Alice', age: 30 })
|
|
494
464
|
const greeting = () => `Hello ${user.get().name}!`
|
|
495
|
-
const cleanup =
|
|
465
|
+
const cleanup = createEffect(() => {
|
|
496
466
|
console.log(`${greeting()} You are ${user.get().age} years old`)
|
|
497
467
|
return () => console.log('Cleanup') // Cleanup function
|
|
498
468
|
})
|
|
@@ -510,10 +480,10 @@ user.set({ name: 'Bob', age: 28 }) // Won't trigger the effect anymore
|
|
|
510
480
|
The `resolve()` function extracts values from multiple signals and returns a discriminated union result:
|
|
511
481
|
|
|
512
482
|
```js
|
|
513
|
-
import {
|
|
483
|
+
import { createState, createComputed, resolve } from '@zeix/cause-effect'
|
|
514
484
|
|
|
515
|
-
const name =
|
|
516
|
-
const age =
|
|
485
|
+
const name = createState('Alice')
|
|
486
|
+
const age = createComputed(() => 30)
|
|
517
487
|
const result = resolve({ name, age })
|
|
518
488
|
|
|
519
489
|
if (result.ok) {
|