@zeix/cause-effect 0.16.1 → 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 +85 -21
- package/.cursorrules +11 -5
- package/.github/copilot-instructions.md +64 -13
- package/CLAUDE.md +143 -163
- package/LICENSE +1 -1
- package/README.md +248 -333
- package/archive/benchmark.ts +688 -0
- package/archive/collection.ts +312 -0
- package/{src → archive}/computed.ts +21 -21
- package/archive/list.ts +551 -0
- package/archive/memo.ts +139 -0
- package/{src → archive}/state.ts +13 -11
- package/archive/store.ts +368 -0
- package/archive/task.ts +194 -0
- package/eslint.config.js +1 -0
- package/index.dev.js +938 -509
- package/index.js +1 -1
- package/index.ts +50 -23
- package/package.json +1 -1
- package/src/classes/collection.ts +282 -0
- package/src/classes/composite.ts +176 -0
- package/src/classes/computed.ts +333 -0
- package/src/classes/list.ts +305 -0
- package/src/classes/ref.ts +68 -0
- package/src/classes/state.ts +98 -0
- package/src/classes/store.ts +210 -0
- package/src/diff.ts +26 -53
- package/src/effect.ts +9 -9
- package/src/errors.ts +71 -25
- package/src/match.ts +5 -12
- package/src/resolve.ts +3 -2
- package/src/signal.ts +58 -41
- package/src/system.ts +79 -42
- package/src/util.ts +16 -34
- package/test/batch.test.ts +15 -17
- package/test/benchmark.test.ts +4 -4
- package/test/collection.test.ts +853 -0
- package/test/computed.test.ts +138 -130
- package/test/diff.test.ts +2 -2
- package/test/effect.test.ts +36 -35
- package/test/list.test.ts +754 -0
- package/test/match.test.ts +25 -25
- package/test/ref.test.ts +227 -0
- package/test/resolve.test.ts +17 -19
- package/test/signal.test.ts +70 -119
- package/test/state.test.ts +44 -44
- package/test/store.test.ts +253 -929
- package/types/index.d.ts +12 -9
- package/types/src/classes/collection.d.ts +46 -0
- package/types/src/classes/composite.d.ts +15 -0
- package/types/src/classes/computed.d.ts +97 -0
- package/types/src/classes/list.d.ts +41 -0
- package/types/src/classes/ref.d.ts +39 -0
- package/types/src/classes/state.d.ts +52 -0
- package/types/src/classes/store.d.ts +51 -0
- package/types/src/diff.d.ts +8 -12
- package/types/src/errors.d.ts +17 -11
- package/types/src/signal.d.ts +27 -14
- package/types/src/system.d.ts +41 -20
- package/types/src/util.d.ts +6 -4
- package/src/store.ts +0 -474
- package/types/src/collection.d.ts +0 -26
- package/types/src/computed.d.ts +0 -33
- package/types/src/scheduler.d.ts +0 -55
- package/types/src/state.d.ts +0 -24
- package/types/src/store.d.ts +0 -65
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Cause & Effect
|
|
2
2
|
|
|
3
|
-
Version 0.
|
|
3
|
+
Version 0.17.1
|
|
4
4
|
|
|
5
|
-
**Cause & Effect** is a
|
|
5
|
+
**Cause & Effect** is a tiny (~5kB gzipped), dependency-free reactive state library for JavaScript. It uses fine-grained signals so derived values and side effects update automatically when their dependencies change.
|
|
6
6
|
|
|
7
7
|
## What is Cause & Effect?
|
|
8
8
|
|
|
@@ -10,31 +10,44 @@ Version 0.16.1
|
|
|
10
10
|
|
|
11
11
|
### Core Concepts
|
|
12
12
|
|
|
13
|
-
- **State
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
13
|
+
- **State**: mutable value (`new State()`)
|
|
14
|
+
- **Memo**: derived & memoized value (`new Memo()`)
|
|
15
|
+
- **Effect**: runs when dependencies change (`createEffect()`)
|
|
16
|
+
- **Task**: async derived value with cancellation (`new Task()`)
|
|
17
|
+
- **Store**: object with reactive nested props (`createStore()`)
|
|
18
|
+
- **List**: mutable array with stable keys & reactive items (`new List()`)
|
|
19
|
+
- **Collection**: read-only derived arrays from Lists (`new DerivedCollection()`)
|
|
20
|
+
- **Ref**: external mutable objects + manual .notify() (`new Ref()`)
|
|
17
21
|
|
|
18
22
|
## Key Features
|
|
19
23
|
|
|
20
|
-
- ⚡ **
|
|
21
|
-
- 🧩 **Composable
|
|
22
|
-
- ⏱️ **Async
|
|
23
|
-
- 🛡️ **
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
|
|
24
|
+
- ⚡ **Fine-grained reactivity** with automatic dependency tracking
|
|
25
|
+
- 🧩 **Composable signal graph** with a small API
|
|
26
|
+
- ⏱️ **Async ready** (`Task`, `AbortController`, async `DerivedCollection`)
|
|
27
|
+
- 🛡️ **Declarative error handling** (`resolve()` + `match()`)
|
|
28
|
+
- 🚀 **Batching** and efficient dependency tracking
|
|
29
|
+
- 📦 **Tree-shakable**, zero dependencies
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# with npm
|
|
35
|
+
npm install @zeix/cause-effect
|
|
36
|
+
|
|
37
|
+
# or with bun
|
|
38
|
+
bun add @zeix/cause-effect
|
|
39
|
+
```
|
|
27
40
|
|
|
28
41
|
## Quick Start
|
|
29
42
|
|
|
30
43
|
```js
|
|
31
|
-
import {
|
|
44
|
+
import { createEffect, Memo, State } from '@zeix/cause-effect'
|
|
32
45
|
|
|
33
46
|
// 1. Create state
|
|
34
|
-
const user =
|
|
47
|
+
const user = new State({ name: 'Alice', age: 30 })
|
|
35
48
|
|
|
36
49
|
// 2. Create computed values
|
|
37
|
-
const greeting =
|
|
50
|
+
const greeting = new Memo(() => `Hello ${user.get().name}!`)
|
|
38
51
|
|
|
39
52
|
// 3. React to changes
|
|
40
53
|
createEffect(() => {
|
|
@@ -45,422 +58,334 @@ createEffect(() => {
|
|
|
45
58
|
user.update(u => ({ ...u, age: 31 })) // Logs: "Hello Alice! You are 31 years old"
|
|
46
59
|
```
|
|
47
60
|
|
|
48
|
-
## Installation
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
# with npm
|
|
52
|
-
npm install @zeix/cause-effect
|
|
53
|
-
|
|
54
|
-
# or with bun
|
|
55
|
-
bun add @zeix/cause-effect
|
|
56
|
-
```
|
|
57
|
-
|
|
58
61
|
## Usage of Signals
|
|
59
62
|
|
|
60
|
-
### State
|
|
63
|
+
### State
|
|
61
64
|
|
|
62
|
-
`
|
|
65
|
+
A `State` is 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
66
|
|
|
64
67
|
```js
|
|
65
|
-
import {
|
|
68
|
+
import { createEffect, State } from '@zeix/cause-effect'
|
|
69
|
+
|
|
70
|
+
const count = new State(42)
|
|
71
|
+
|
|
72
|
+
createEffect(() => console.log(count.get()))
|
|
73
|
+
count.set(24)
|
|
66
74
|
|
|
67
|
-
const count = createState(42)
|
|
68
|
-
createEffect(() => {
|
|
69
|
-
console.log(count.get()) // logs '42'
|
|
70
|
-
})
|
|
71
|
-
count.set(24) // logs '24'
|
|
72
75
|
document.querySelector('.increment').addEventListener('click', () => {
|
|
73
76
|
count.update(v => ++v)
|
|
74
77
|
})
|
|
75
|
-
// Click on button logs '25', '26', and so on
|
|
76
78
|
```
|
|
77
79
|
|
|
78
|
-
|
|
80
|
+
Use `State` for primitives or for objects you typically replace entirely.
|
|
79
81
|
|
|
80
|
-
|
|
82
|
+
### Memo
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
import { createStore, createEffect } from '@zeix/cause-effect'
|
|
84
|
+
A `Memo` is a memoized read-only signal that automatically tracks dependencies and updates only when those dependencies change.
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
age: 30,
|
|
88
|
-
preferences: {
|
|
89
|
-
theme: 'dark',
|
|
90
|
-
notifications: true
|
|
91
|
-
}
|
|
92
|
-
})
|
|
86
|
+
```js
|
|
87
|
+
import { State, Memo, createEffect } from '@zeix/cause-effect'
|
|
93
88
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
console.log(`${user.name.get()} is ${user.age.get()} years old`)
|
|
97
|
-
})
|
|
89
|
+
const count = new State(42)
|
|
90
|
+
const isEven = new Memo(() => !(count.get() % 2))
|
|
98
91
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
})
|
|
92
|
+
createEffect(() => console.log(isEven.get()))
|
|
93
|
+
count.set(24) // no log; still even
|
|
94
|
+
```
|
|
103
95
|
|
|
104
|
-
|
|
105
|
-
user.age.update(v => v + 1) // Logs: "Alice is 31 years old"
|
|
106
|
-
user.preferences.theme.set('light') // Logs: "Theme: light"
|
|
96
|
+
**Tip**: For simple derivations, a plain function can be faster:
|
|
107
97
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
console.log('User data:', user.get()) // Triggers on any nested change
|
|
111
|
-
})
|
|
98
|
+
```js
|
|
99
|
+
const isEven = () => !(count.get() % 2)
|
|
112
100
|
```
|
|
113
101
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
**When to use stores vs states:**
|
|
102
|
+
**Advanced**: Reducer-style memos:
|
|
117
103
|
|
|
118
|
-
|
|
119
|
-
|
|
104
|
+
```js
|
|
105
|
+
import { State, Memo } from '@zeix/cause-effect'
|
|
106
|
+
|
|
107
|
+
const actions = new State('reset')
|
|
108
|
+
const counter = new Memo((prev) => {
|
|
109
|
+
switch (actions.get()) {
|
|
110
|
+
case 'increment': return prev + 1
|
|
111
|
+
case 'decrement': return prev - 1
|
|
112
|
+
case 'reset': return 0
|
|
113
|
+
default: return prev
|
|
114
|
+
}
|
|
115
|
+
}, 0)
|
|
116
|
+
```
|
|
120
117
|
|
|
121
|
-
|
|
118
|
+
### Task
|
|
122
119
|
|
|
123
|
-
|
|
120
|
+
A `Task` handles asynchronous computations with cancellation support:
|
|
124
121
|
|
|
125
122
|
```js
|
|
126
|
-
import {
|
|
123
|
+
import { State, Task } from '@zeix/cause-effect'
|
|
127
124
|
|
|
128
|
-
const
|
|
125
|
+
const id = new State(1)
|
|
129
126
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
settings.add('autoSave', false) // Ignored - autoSave remains true
|
|
136
|
-
|
|
137
|
-
// Remove properties
|
|
138
|
-
settings.remove('timeout')
|
|
139
|
-
console.log(settings.timeout) // undefined
|
|
127
|
+
const data = new Task(async (oldValue, abort) => {
|
|
128
|
+
const response = await fetch(`/api/users/${id.get()}`, { signal: abort })
|
|
129
|
+
if (!response.ok) throw new Error('Failed to fetch')
|
|
130
|
+
return response.json()
|
|
131
|
+
})
|
|
140
132
|
|
|
141
|
-
//
|
|
142
|
-
settings.remove('nonExistent') // Safe - no error thrown
|
|
133
|
+
id.set(2) // cancels previous fetch automatically
|
|
143
134
|
```
|
|
144
135
|
|
|
145
|
-
|
|
146
|
-
- They bypass the full reconciliation process used by `set()` and `update()`
|
|
147
|
-
- They're perfect for frequent single-property additions/removals
|
|
148
|
-
- They trigger the same events and reactivity as other store operations
|
|
136
|
+
**Note**: Use Task (not plain async functions) when you want memoization + cancellation + reactive pending/error states.
|
|
149
137
|
|
|
150
|
-
|
|
138
|
+
### Store
|
|
151
139
|
|
|
152
|
-
|
|
140
|
+
A `Store` is a reactive object. Each property automatically becomes its own signal with `.get()`, `.set()`, and `.update()` methods. Nested objects recursively become nested stores.
|
|
153
141
|
|
|
154
142
|
```js
|
|
155
143
|
import { createStore, createEffect } from '@zeix/cause-effect'
|
|
156
144
|
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
145
|
+
const user = createStore({
|
|
146
|
+
name: 'Alice',
|
|
147
|
+
age: 30,
|
|
148
|
+
preferences: { theme: 'dark', notifications: true }
|
|
149
|
+
})
|
|
162
150
|
|
|
163
|
-
// Individual items are reactive
|
|
164
151
|
createEffect(() => {
|
|
165
|
-
console.log(
|
|
152
|
+
console.log(`${user.name.get()} is ${user.age.get()} years old`)
|
|
166
153
|
})
|
|
167
154
|
|
|
168
|
-
|
|
169
|
-
|
|
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']
|
|
155
|
+
user.age.update(v => v + 1)
|
|
156
|
+
user.preferences.theme.set('light')
|
|
175
157
|
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
console.log(items.get()) // ['date', 'cherry', 'banana', 'apple']
|
|
158
|
+
// Watch the full object
|
|
159
|
+
createEffect(() => console.log('User:', user.get()))
|
|
179
160
|
```
|
|
180
161
|
|
|
181
|
-
|
|
182
|
-
|
|
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:
|
|
162
|
+
Dynamic properties using the `add()` and `remove()` methods:
|
|
184
163
|
|
|
185
164
|
```js
|
|
186
|
-
|
|
165
|
+
const settings = createStore({ autoSave: true })
|
|
187
166
|
|
|
188
|
-
|
|
167
|
+
settings.add('timeout', 5000)
|
|
168
|
+
settings.remove('timeout')
|
|
169
|
+
```
|
|
189
170
|
|
|
190
|
-
|
|
191
|
-
const offAdd = user.on('add', (added) => {
|
|
192
|
-
console.log('Added properties:', added)
|
|
193
|
-
})
|
|
171
|
+
Change Notifications using the `.on()` method:
|
|
194
172
|
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
console.log('Changed properties:', changed)
|
|
198
|
-
})
|
|
173
|
+
```js
|
|
174
|
+
const user = createStore({ name: 'Alice', age: 30 })
|
|
199
175
|
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
})
|
|
176
|
+
const offChange = user.on('change', changed => console.log(changed))
|
|
177
|
+
const offAdd = user.on('add', added => console.log(added))
|
|
178
|
+
const offRemove = user.on('remove', removed => console.log(removed))
|
|
204
179
|
|
|
205
180
|
// These will trigger the respective notifications:
|
|
206
|
-
user.add('email', 'alice@example.com') // Logs: "Added properties:
|
|
207
|
-
user.age.set(31) // Logs: "Changed properties:
|
|
208
|
-
user.remove('email') // Logs: "Removed properties:
|
|
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
|
-
})
|
|
216
|
-
```
|
|
181
|
+
user.add('email', 'alice@example.com') // Logs: "Added properties: ['email']"
|
|
182
|
+
user.age.set(31) // Logs: "Changed properties: ['age']"
|
|
183
|
+
user.remove('email') // Logs: "Removed properties: ['email']"
|
|
217
184
|
|
|
218
|
-
|
|
185
|
+
To stop listening to notifications, call the returned cleanup functions:
|
|
219
186
|
|
|
220
187
|
```js
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
//
|
|
224
|
-
// Logs: "Added properties: { city: 'New York' }"
|
|
188
|
+
offAdd() // Stop listening to add notifications
|
|
189
|
+
offChange() // Stop listening to change notifications
|
|
190
|
+
offRemove() // Stop listening to remove notifications
|
|
225
191
|
```
|
|
226
192
|
|
|
227
|
-
|
|
193
|
+
### List
|
|
228
194
|
|
|
229
|
-
|
|
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
|
-
```
|
|
195
|
+
A `List` is a mutable signal for arrays with individually reactive items and stable keys. Each item becomes its own signal while maintaining persistent identity through sorting and reordering:
|
|
235
196
|
|
|
236
|
-
|
|
197
|
+
```js
|
|
198
|
+
import { List, createEffect } from '@zeix/cause-effect'
|
|
237
199
|
|
|
238
|
-
|
|
200
|
+
const items = new List(['banana', 'apple', 'cherry'])
|
|
239
201
|
|
|
240
|
-
|
|
241
|
-
import { createState, createComputed, createEffect } from '@zeix/cause-effect'
|
|
202
|
+
createEffect(() => console.log(`First: ${items[0].get()}`))
|
|
242
203
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
count.set(24) // logs nothing because 24 is also an even number
|
|
247
|
-
document.querySelector('button.increment').addEventListener('click', () => {
|
|
248
|
-
count.update(v => ++v)
|
|
249
|
-
})
|
|
250
|
-
// Click on button logs 'false', 'true', and so on
|
|
204
|
+
items.add('date')
|
|
205
|
+
items.splice(1, 1, 'orange')
|
|
206
|
+
items.sort()
|
|
251
207
|
```
|
|
252
208
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
**Performance tip**: For simple derivations, plain functions often outperform computed signals:
|
|
209
|
+
Keys are stable across reordering:
|
|
256
210
|
|
|
257
211
|
```js
|
|
258
|
-
|
|
259
|
-
const
|
|
212
|
+
const items = new List(['banana', 'apple'], 'item-')
|
|
213
|
+
const key = items.add('orange')
|
|
214
|
+
|
|
215
|
+
items.sort()
|
|
216
|
+
console.log(items.byKey(key)) // 'orange'
|
|
217
|
+
console.log(items.indexOfKey(key)) // current index
|
|
260
218
|
```
|
|
261
219
|
|
|
262
|
-
|
|
220
|
+
Lists have `.add()`, `.remove()` and `.on()` methods like stores. In addition, they have `.sort()` and `.splice()` methods. But unlike stores, deeply nested properties in items are not converted to individual signals.
|
|
263
221
|
|
|
264
|
-
|
|
265
|
-
- **Use createComputed() when**:
|
|
266
|
-
- The calculation is expensive
|
|
267
|
-
- You need to share the result between multiple consumers
|
|
268
|
-
- You're working with asynchronous operations
|
|
269
|
-
- You need to track specific error states
|
|
270
|
-
|
|
271
|
-
#### Reducer-like Capabilities
|
|
222
|
+
### Collection
|
|
272
223
|
|
|
273
|
-
`
|
|
224
|
+
A `Collection` is a read-only derived reactive list from `List` or another `Collection`:
|
|
274
225
|
|
|
275
226
|
```js
|
|
276
|
-
import {
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
|
227
|
+
import { List, createEffect } from '@zeix/cause-effect'
|
|
228
|
+
|
|
229
|
+
const users = new List([
|
|
230
|
+
{ id: 1, name: 'Alice', role: 'admin' },
|
|
231
|
+
{ id: 2, name: 'Bob', role: 'user' }
|
|
232
|
+
])
|
|
233
|
+
const profiles = users.deriveCollection(user => ({
|
|
234
|
+
...user,
|
|
235
|
+
displayName: `${user.name} (${user.role})`
|
|
236
|
+
}))
|
|
237
|
+
|
|
238
|
+
createEffect(() => console.log('Profiles:', profiles.get()))
|
|
239
|
+
console.log(userProfiles.at(0).get().displayName)
|
|
300
240
|
```
|
|
301
241
|
|
|
302
|
-
|
|
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
|
|
242
|
+
Async mapping is supported:
|
|
307
243
|
|
|
308
|
-
|
|
244
|
+
```js
|
|
245
|
+
const details = users.derivedCollection(async (user, abort) => {
|
|
246
|
+
const response = await fetch(`/users/${user.id}`, { signal: abort })
|
|
247
|
+
return { ...user, details: await response.json() }
|
|
248
|
+
})
|
|
249
|
+
```
|
|
309
250
|
|
|
310
|
-
|
|
251
|
+
### Ref
|
|
311
252
|
|
|
312
|
-
|
|
313
|
-
2. Automatically cancels pending operations when dependencies change
|
|
314
|
-
3. Returns `UNSET` while the Promise is pending
|
|
315
|
-
4. Properly handles errors from failed requests
|
|
253
|
+
A `Ref` is a signal that holds a reference to an external object that can change outside the reactive system.
|
|
316
254
|
|
|
317
255
|
```js
|
|
318
|
-
import {
|
|
256
|
+
import { createEffect, Ref } from '@zeix/cause-effect'
|
|
319
257
|
|
|
320
|
-
const
|
|
321
|
-
const data = createComputed(async (_, abort) => {
|
|
322
|
-
// The abort signal is automatically managed by the computed signal
|
|
323
|
-
const response = await fetch(`/api/entries/${id.get()}`, { signal: abort })
|
|
324
|
-
if (!response.ok) throw new Error(`Failed to fetch data: ${response.statusText}`)
|
|
325
|
-
return response.json()
|
|
326
|
-
})
|
|
258
|
+
const elementRef = new Ref(document.getElementById('status'))
|
|
327
259
|
|
|
328
|
-
|
|
329
|
-
createEffect(() => {
|
|
330
|
-
match(resolve({ data }), {
|
|
331
|
-
ok: ({ data: json }) => console.log('Data loaded:', json),
|
|
332
|
-
nil: () => console.log('Loading...'),
|
|
333
|
-
err: errors => console.error('Error:', errors[0])
|
|
334
|
-
})
|
|
335
|
-
})
|
|
260
|
+
createEffect(() => console.log(elementRef.get().className))
|
|
336
261
|
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
id.update(v => ++v)
|
|
340
|
-
})
|
|
262
|
+
// external mutation happened
|
|
263
|
+
elementRef.notify()
|
|
341
264
|
```
|
|
342
265
|
|
|
343
|
-
|
|
266
|
+
Use `Ref` for DOM nodes, Maps/Sets, sockets, third-party objects, etc.
|
|
344
267
|
|
|
345
|
-
## Effects
|
|
268
|
+
## Effects
|
|
346
269
|
|
|
347
|
-
The `createEffect()`
|
|
348
|
-
|
|
349
|
-
### Synchronous Effects
|
|
270
|
+
The `createEffect()` callback runs whenever the signals it reads change. It supports sync or async callbacks and returns a cleanup function.
|
|
350
271
|
|
|
351
272
|
```js
|
|
352
|
-
import {
|
|
273
|
+
import { State, createEffect } from '@zeix/cause-effect'
|
|
353
274
|
|
|
354
|
-
const count =
|
|
355
|
-
|
|
356
|
-
|
|
275
|
+
const count = new State(42)
|
|
276
|
+
|
|
277
|
+
const cleanup = createEffect(() => {
|
|
278
|
+
console.log(count.get())
|
|
279
|
+
return () => console.log('Cleanup')
|
|
357
280
|
})
|
|
358
|
-
```
|
|
359
281
|
|
|
360
|
-
|
|
282
|
+
cleanup()
|
|
283
|
+
```
|
|
361
284
|
|
|
362
|
-
Async
|
|
285
|
+
Async effects receive an AbortSignal that cancels on rerun or cleanup:
|
|
363
286
|
|
|
364
287
|
```js
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
createEffect(async (abort) => {
|
|
369
|
-
try {
|
|
370
|
-
const response = await fetch(`/api/users/${userId.get()}`, { signal: abort })
|
|
371
|
-
const user = await response.json()
|
|
372
|
-
console.log('User loaded:', user)
|
|
373
|
-
} catch (error) {
|
|
374
|
-
if (!abort.aborted) {
|
|
375
|
-
console.error('Failed to load user:', error)
|
|
376
|
-
}
|
|
377
|
-
}
|
|
288
|
+
createEffect(async abort => {
|
|
289
|
+
const res = await fetch('/api', { signal: abort })
|
|
290
|
+
if (res.ok) console.log(await res.json())
|
|
378
291
|
})
|
|
379
292
|
```
|
|
380
293
|
|
|
381
|
-
### Error Handling
|
|
294
|
+
### Error Handling: resolve() + match()
|
|
382
295
|
|
|
383
|
-
|
|
296
|
+
Use `resolve()` to extract values from signals (including pending/err states) and `match()` to handle them declaratively:
|
|
384
297
|
|
|
385
298
|
```js
|
|
386
|
-
import {
|
|
299
|
+
import { State, Task, createEffect, resolve, match } from '@zeix/cause-effect'
|
|
387
300
|
|
|
388
|
-
const userId =
|
|
389
|
-
const userData =
|
|
390
|
-
const
|
|
391
|
-
if (!
|
|
392
|
-
return
|
|
301
|
+
const userId = new State(1)
|
|
302
|
+
const userData = new Task(async (_, abort) => {
|
|
303
|
+
const res = await fetch(`/api/users/${userId.get()}`, { signal: abort })
|
|
304
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
305
|
+
return res.json()
|
|
393
306
|
})
|
|
394
307
|
|
|
395
308
|
createEffect(() => {
|
|
396
309
|
match(resolve({ userData }), {
|
|
397
|
-
ok: ({ userData: user }) => console.log('User
|
|
398
|
-
nil: () => console.log('Loading
|
|
399
|
-
err: errors => console.error(
|
|
310
|
+
ok: ({ userData: user }) => console.log('User:', user),
|
|
311
|
+
nil: () => console.log('Loading...'),
|
|
312
|
+
err: errors => console.error(errors[0])
|
|
400
313
|
})
|
|
401
314
|
})
|
|
402
315
|
```
|
|
403
316
|
|
|
404
|
-
|
|
317
|
+
## Signal Type Decision Tree
|
|
318
|
+
|
|
319
|
+
```
|
|
320
|
+
Is the value managed *inside* the reactive system?
|
|
321
|
+
│
|
|
322
|
+
├─ No → Use `Ref`
|
|
323
|
+
│ (DOM nodes, Map/Set, Date, sockets, 3rd-party objects)
|
|
324
|
+
│ Remember: call `.notify()` when it changes externally.
|
|
325
|
+
│
|
|
326
|
+
└─ Yes? What kind of data is it?
|
|
327
|
+
│
|
|
328
|
+
├─ *Primitive* (number/string/boolean)
|
|
329
|
+
│ │
|
|
330
|
+
│ ├─ Do you want to mutate it directly?
|
|
331
|
+
│ │ └─ Yes → `State`
|
|
332
|
+
│ │
|
|
333
|
+
│ └─ Is it derived from other signals?
|
|
334
|
+
│ │
|
|
335
|
+
│ ├─ Sync derived
|
|
336
|
+
│ │ ├─ Simple/cheap → plain function (preferred)
|
|
337
|
+
│ │ └─ Expensive/shared/stateful → `Memo`
|
|
338
|
+
│ │
|
|
339
|
+
│ └─ Async derived → `Task`
|
|
340
|
+
│ (cancellation + memoization + pending/error state)
|
|
341
|
+
│
|
|
342
|
+
├─ *Plain Object*
|
|
343
|
+
│ │
|
|
344
|
+
│ ├─ Do you want to mutate individual properties?
|
|
345
|
+
│ │ ├─ Yes → `Store`
|
|
346
|
+
│ │ └─ No, whole object mutations only → `State`
|
|
347
|
+
│ │
|
|
348
|
+
│ └─ Is it derived from other signals?
|
|
349
|
+
│ ├─ Sync derived → plain function or `Memo`
|
|
350
|
+
│ └─ Async derived → `Task`
|
|
351
|
+
│
|
|
352
|
+
└─ *Array*
|
|
353
|
+
│
|
|
354
|
+
├─ Do you need to mutate it (add/remove/sort) with stable item identity?
|
|
355
|
+
│ ├─ Yes → `List`
|
|
356
|
+
│ └─ No, whole array mutations only → `State`
|
|
357
|
+
│
|
|
358
|
+
└─ Is it derived / read-only transformation of a `List` or `Collection`?
|
|
359
|
+
└─ Yes → `Collection`
|
|
360
|
+
(memoized + supports async mapping + chaining)
|
|
361
|
+
```
|
|
405
362
|
|
|
406
363
|
## Advanced Usage
|
|
407
364
|
|
|
408
|
-
### Batching
|
|
365
|
+
### Batching
|
|
409
366
|
|
|
410
|
-
|
|
367
|
+
Group multiple signal updates, ensuring effects run only once after all changes are applied:
|
|
411
368
|
|
|
412
369
|
```js
|
|
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)]
|
|
424
|
-
|
|
425
|
-
// Compute the sum of all signals
|
|
426
|
-
const sum = createComputed(() => {
|
|
427
|
-
const v = signals.reduce((total, signal) => total + signal.get(), 0)
|
|
428
|
-
// Validate the result
|
|
429
|
-
if (!Number.isFinite(v)) throw new Error('Invalid value')
|
|
430
|
-
return v
|
|
431
|
-
})
|
|
370
|
+
import { batchSignalWrites, State } from '@zeix/cause-effect'
|
|
432
371
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
match(resolve({ sum }), {
|
|
436
|
-
ok: ({ sum: v }) => console.log('Sum:', v),
|
|
437
|
-
err: errors => console.error('Error:', errors[0])
|
|
438
|
-
})
|
|
439
|
-
})
|
|
372
|
+
const a = new State(2)
|
|
373
|
+
const b = new State(3)
|
|
440
374
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
signals.forEach(signal => {
|
|
445
|
-
signal.update(v => v * 2)
|
|
446
|
-
})
|
|
447
|
-
})
|
|
375
|
+
batchSignalWrites(() => {
|
|
376
|
+
a.set(4)
|
|
377
|
+
b.set(5)
|
|
448
378
|
})
|
|
449
|
-
// Click on button logs '20' only once
|
|
450
|
-
// (instead of first '12', then '15' and then '20' without batch)
|
|
451
|
-
|
|
452
|
-
// Provoke an error - but no worries: it will be handled fine
|
|
453
|
-
signals[0].set(NaN)
|
|
454
379
|
```
|
|
455
380
|
|
|
456
|
-
### Cleanup
|
|
381
|
+
### Cleanup
|
|
457
382
|
|
|
458
383
|
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.
|
|
459
384
|
|
|
460
385
|
```js
|
|
461
|
-
import {
|
|
386
|
+
import { State, createEffect } from '@zeix/cause-effect'
|
|
462
387
|
|
|
463
|
-
const user =
|
|
388
|
+
const user = new State({ name: 'Alice', age: 30 })
|
|
464
389
|
const greeting = () => `Hello ${user.get().name}!`
|
|
465
390
|
const cleanup = createEffect(() => {
|
|
466
391
|
console.log(`${greeting()} You are ${user.get().age} years old`)
|
|
@@ -473,31 +398,25 @@ cleanup() // Logs: 'Cleanup' and unsubscribes from signal `user`
|
|
|
473
398
|
user.set({ name: 'Bob', age: 28 }) // Won't trigger the effect anymore
|
|
474
399
|
```
|
|
475
400
|
|
|
476
|
-
|
|
401
|
+
### resolve()
|
|
477
402
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
The `resolve()` function extracts values from multiple signals and returns a discriminated union result:
|
|
403
|
+
Extract signal values:
|
|
481
404
|
|
|
482
405
|
```js
|
|
483
|
-
import {
|
|
406
|
+
import { State, Memo, resolve } from '@zeix/cause-effect'
|
|
484
407
|
|
|
485
|
-
const name =
|
|
486
|
-
const age =
|
|
408
|
+
const name = new State('Alice')
|
|
409
|
+
const age = new Memo(() => 30)
|
|
487
410
|
const result = resolve({ name, age })
|
|
488
411
|
|
|
489
|
-
if (result.ok)
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
console.log('Loading...')
|
|
493
|
-
} else {
|
|
494
|
-
console.error('Errors:', result.errors)
|
|
495
|
-
}
|
|
412
|
+
if (result.ok) console.log(result.values.name, result.values.age)
|
|
413
|
+
else if (result.pending) console.log('Loading...')
|
|
414
|
+
else console.error('Errors:', result.errors)
|
|
496
415
|
```
|
|
497
416
|
|
|
498
|
-
###
|
|
417
|
+
### match()
|
|
499
418
|
|
|
500
|
-
|
|
419
|
+
Pattern matching on resolved results for side effects:
|
|
501
420
|
|
|
502
421
|
```js
|
|
503
422
|
import { resolve, match } from '@zeix/cause-effect'
|
|
@@ -509,9 +428,9 @@ match(resolve({ name, age }), {
|
|
|
509
428
|
})
|
|
510
429
|
```
|
|
511
430
|
|
|
512
|
-
###
|
|
431
|
+
### diff()
|
|
513
432
|
|
|
514
|
-
|
|
433
|
+
Compare object changes:
|
|
515
434
|
|
|
516
435
|
```js
|
|
517
436
|
import { diff } from '@zeix/cause-effect'
|
|
@@ -526,11 +445,9 @@ console.log(changes.change) // { age: 31 }
|
|
|
526
445
|
console.log(changes.remove) // { city: UNSET }
|
|
527
446
|
```
|
|
528
447
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
### `isEqual()` - Deep Equality Comparison
|
|
448
|
+
### isEqual()
|
|
532
449
|
|
|
533
|
-
|
|
450
|
+
Deep equality comparison with circular reference detection:
|
|
534
451
|
|
|
535
452
|
```js
|
|
536
453
|
import { isEqual } from '@zeix/cause-effect'
|
|
@@ -548,12 +465,10 @@ console.log(isEqual('hello', 'hello')) // true
|
|
|
548
465
|
console.log(isEqual({ a: [1, 2] }, { a: [1, 2] })) // true
|
|
549
466
|
```
|
|
550
467
|
|
|
551
|
-
Both `diff()` and `isEqual()` include built-in protection against circular references and will throw a `CircularDependencyError` if cycles are detected.
|
|
552
|
-
|
|
553
468
|
## Contributing & License
|
|
554
469
|
|
|
555
470
|
Feel free to contribute, report issues, or suggest improvements.
|
|
556
471
|
|
|
557
472
|
License: [MIT](LICENSE)
|
|
558
473
|
|
|
559
|
-
(c)
|
|
474
|
+
(c) 2024 - 2026 [Zeix AG](https://zeix.com)
|