@zeix/cause-effect 0.15.2 → 0.16.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Cause & Effect
2
2
 
3
- Version 0.15.2
3
+ Version 0.16.1
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: `state()`
14
- - **Store signals**: Hold objects of nested reactive properties: `store()`
15
- - **Computed signals**: Derive memoized values from other signals: `computed()`
16
- - **Effects**: Run side effects when signals change: `effect()`
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 { state, computed, effect } from '@zeix/cause-effect'
31
+ import { createState, createComputed, createEffect } from '@zeix/cause-effect'
32
32
 
33
33
  // 1. Create state
34
- const user = state({ name: 'Alice', age: 30 })
34
+ const user = createState({ name: 'Alice', age: 30 })
35
35
 
36
36
  // 2. Create computed values
37
- const greeting = computed(() => `Hello ${user.get().name}!`)
37
+ const greeting = createComputed(() => `Hello ${user.get().name}!`)
38
38
 
39
39
  // 3. React to changes
40
- effect(() => {
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
- `state()` 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.
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 { state, effect } from '@zeix/cause-effect'
65
+ import { createState, createEffect } from '@zeix/cause-effect'
66
66
 
67
- const count = state(42)
68
- effect(() => {
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
- `store()` 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.
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 { store, effect } from '@zeix/cause-effect'
83
+ import { createStore, createEffect } from '@zeix/cause-effect'
84
84
 
85
- const user = store({
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
- effect(() => {
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
- effect(() => {
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
- effect(() => {
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 { store, effect } from '@zeix/cause-effect'
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 { store, effect } from '@zeix/cause-effect'
155
+ import { createStore, createEffect } from '@zeix/cause-effect'
149
156
 
150
- const items = store(['banana', 'apple', 'cherry'])
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
- effect(() => {
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 Events
181
+ #### Store Change Notifications
175
182
 
176
- Stores emit events when properties are added, changed, or removed. You can listen to these events using standard `addEventListener()`:
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 { store } from '@zeix/cause-effect'
186
+ import { createStore } from '@zeix/cause-effect'
180
187
 
181
- const user = store({ name: 'Alice', age: 30 })
188
+ const user = createStore({ name: 'Alice', age: 30 })
182
189
 
183
190
  // Listen for property additions
184
- user.addEventListener('store-add', (event) => {
185
- console.log('Added properties:', event.detail)
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.addEventListener('store-change', (event) => {
190
- console.log('Changed properties:', event.detail)
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.addEventListener('store-remove', (event) => {
195
- console.log('Removed properties:', event.detail)
201
+ const offRemove = user.on('remove', (removed) => {
202
+ console.log('Removed properties:', removed)
196
203
  })
197
204
 
198
- // These will trigger the respective events:
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 events (useful for UI animations)
204
- const items = store(['banana', 'apple', 'cherry'])
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.addEventListener('store-sort', (event) => {
207
- console.log('Items reordered:', event.detail) // ['2', '1', '0']
213
+ const offSort = items.on('sort', (newOrder) => {
214
+ console.log('Items reordered:', newOrder) // ['2', '1', '0']
208
215
  })
209
216
  ```
210
217
 
211
- Events are also fired when using `set()` or `update()` methods on the entire store:
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 events based on what changed
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
- **When to use stores vs state:**
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
- ### Computed Signals vs. Functions
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
- #### When to Use Computed Signals
236
+ ### Computed Signals
227
237
 
228
- `computed()` creates a memoized read-only signal that automatically tracks dependencies and updates only when those dependencies change.
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 { state, computed, effect } from '@zeix/cause-effect'
241
+ import { createState, createComputed, createEffect } from '@zeix/cause-effect'
232
242
 
233
- const count = state(42)
234
- const isEven = computed(() => !(count.get() % 2))
235
- effect(() => console.log(isEven.get())) // logs 'true'
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 Functions
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 computed() when**:
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
- `computed()` seamlessly handles asynchronous operations with built-in cancellation support. When used with an async function, it:
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 { state, computed, effect, resolve, match } from '@zeix/cause-effect'
318
+ import { createState, createComputed, createEffect, resolve, match } from '@zeix/cause-effect'
272
319
 
273
- const id = state(42)
274
- const data = computed(async abort => {
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
- effect(() => {
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 `computed()` (not plain functions) for async operations to benefit from automatic cancellation, memoization, and state management.
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 `effect()` function supports both synchronous and asynchronous callbacks:
347
+ The `createEffect()` function supports both synchronous and asynchronous callbacks:
301
348
 
302
349
  ### Synchronous Effects
303
350
 
304
351
  ```js
305
- import { state, effect } from '@zeix/cause-effect'
352
+ import { createState, createEffect } from '@zeix/cause-effect'
306
353
 
307
- const count = state(42)
308
- effect(() => {
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 { state, effect } from '@zeix/cause-effect'
365
+ import { createState, createEffect } from '@zeix/cause-effect'
319
366
 
320
- const userId = state(1)
321
- effect(async (abort) => {
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 { state, computed, effect, resolve, match } from '@zeix/cause-effect'
386
+ import { createState, createEffect, resolve, match } from '@zeix/cause-effect'
340
387
 
341
- const userId = state(1)
342
- const userData = computed(async (abort) => {
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
- effect(() => {
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 { state, computed, effect, batch, resolve, match } from '@zeix/cause-effect'
451
-
452
- // State: define an array of State<number>
453
- const signals = [state(2), state(3), state(5)]
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 = computed(() => {
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
- effect(() => {
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 { state, computed, effect } from '@zeix/cause-effect'
461
+ import { createState, createComputed, createEffect } from '@zeix/cause-effect'
492
462
 
493
- const user = state({ name: 'Alice', age: 30 })
463
+ const user = createState({ name: 'Alice', age: 30 })
494
464
  const greeting = () => `Hello ${user.get().name}!`
495
- const cleanup = effect(() => {
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 { state, computed, resolve } from '@zeix/cause-effect'
483
+ import { createState, createComputed, resolve } from '@zeix/cause-effect'
514
484
 
515
- const name = state('Alice')
516
- const age = computed(() => 30)
485
+ const name = createState('Alice')
486
+ const age = createComputed(() => 30)
517
487
  const result = resolve({ name, age })
518
488
 
519
489
  if (result.ok) {
package/eslint.config.js CHANGED
@@ -1,5 +1,5 @@
1
- import globals from 'globals'
2
1
  import pluginJs from '@eslint/js'
2
+ import globals from 'globals'
3
3
  import tseslint from 'typescript-eslint'
4
4
 
5
5
  /** @type {import('eslint').Linter.Config[]} */