@zeix/cause-effect 0.17.2 → 0.18.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 +163 -226
- package/.cursorrules +41 -35
- package/.github/copilot-instructions.md +166 -116
- package/.zed/settings.json +3 -0
- package/ARCHITECTURE.md +274 -0
- package/CLAUDE.md +197 -202
- package/COLLECTION_REFACTORING.md +161 -0
- package/GUIDE.md +298 -0
- package/README.md +241 -220
- package/REQUIREMENTS.md +100 -0
- package/bench/reactivity.bench.ts +577 -0
- package/index.dev.js +1326 -1174
- package/index.js +1 -1
- package/index.ts +58 -85
- package/package.json +9 -6
- package/src/errors.ts +118 -70
- package/src/graph.ts +601 -0
- package/src/nodes/collection.ts +474 -0
- package/src/nodes/effect.ts +149 -0
- package/src/nodes/list.ts +588 -0
- package/src/nodes/memo.ts +120 -0
- package/src/nodes/sensor.ts +139 -0
- package/src/nodes/state.ts +135 -0
- package/src/nodes/store.ts +383 -0
- package/src/nodes/task.ts +146 -0
- package/src/signal.ts +112 -64
- package/src/util.ts +26 -57
- package/test/batch.test.ts +96 -69
- package/test/benchmark.test.ts +473 -485
- package/test/collection.test.ts +455 -955
- package/test/effect.test.ts +293 -696
- package/test/list.test.ts +332 -857
- package/test/memo.test.ts +380 -0
- package/test/regression.test.ts +156 -0
- package/test/scope.test.ts +191 -0
- package/test/sensor.test.ts +454 -0
- package/test/signal.test.ts +220 -213
- package/test/state.test.ts +217 -271
- package/test/store.test.ts +346 -898
- package/test/task.test.ts +395 -0
- package/test/untrack.test.ts +167 -0
- package/test/util/dependency-graph.ts +2 -2
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +5 -7
- package/types/index.d.ts +13 -15
- package/types/src/errors.d.ts +73 -19
- package/types/src/graph.d.ts +208 -0
- package/types/src/nodes/collection.d.ts +64 -0
- package/types/src/nodes/effect.d.ts +48 -0
- package/types/src/nodes/list.d.ts +65 -0
- package/types/src/nodes/memo.d.ts +57 -0
- package/types/src/nodes/sensor.d.ts +75 -0
- package/types/src/nodes/state.d.ts +78 -0
- package/types/src/nodes/store.d.ts +51 -0
- package/types/src/nodes/task.d.ts +73 -0
- package/types/src/signal.d.ts +43 -28
- package/types/src/util.d.ts +9 -16
- package/archive/benchmark.ts +0 -688
- package/archive/collection.ts +0 -310
- package/archive/computed.ts +0 -198
- package/archive/list.ts +0 -544
- package/archive/memo.ts +0 -140
- package/archive/state.ts +0 -90
- package/archive/store.ts +0 -357
- package/archive/task.ts +0 -191
- package/src/classes/collection.ts +0 -298
- package/src/classes/composite.ts +0 -171
- package/src/classes/computed.ts +0 -392
- package/src/classes/list.ts +0 -310
- package/src/classes/ref.ts +0 -96
- package/src/classes/state.ts +0 -131
- package/src/classes/store.ts +0 -227
- package/src/diff.ts +0 -138
- package/src/effect.ts +0 -96
- package/src/match.ts +0 -45
- package/src/resolve.ts +0 -49
- package/src/system.ts +0 -275
- package/test/computed.test.ts +0 -1126
- package/test/diff.test.ts +0 -955
- package/test/match.test.ts +0 -388
- package/test/ref.test.ts +0 -381
- package/test/resolve.test.ts +0 -154
- package/types/src/classes/collection.d.ts +0 -47
- package/types/src/classes/composite.d.ts +0 -15
- package/types/src/classes/computed.d.ts +0 -114
- package/types/src/classes/list.d.ts +0 -41
- package/types/src/classes/ref.d.ts +0 -48
- package/types/src/classes/state.d.ts +0 -61
- package/types/src/classes/store.d.ts +0 -51
- package/types/src/diff.d.ts +0 -28
- package/types/src/effect.d.ts +0 -15
- package/types/src/match.d.ts +0 -21
- package/types/src/resolve.d.ts +0 -29
- package/types/src/system.d.ts +0 -81
package/README.md
CHANGED
|
@@ -1,32 +1,40 @@
|
|
|
1
1
|
# Cause & Effect
|
|
2
2
|
|
|
3
|
-
Version 0.
|
|
3
|
+
Version 0.18.0
|
|
4
4
|
|
|
5
|
-
**Cause & Effect** is a
|
|
5
|
+
**Cause & Effect** is a reactive state management primitives library for TypeScript. It provides the foundational building blocks for managing complex, dynamic, composite, and asynchronous state — correctly and performantly — in a unified signal graph.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
It is deliberately **not a framework**. It has no opinions about rendering, persistence, or application architecture. It is a thin, trustworthy layer over JavaScript that provides the comfort and guarantees of fine-grained reactivity while avoiding the common pitfalls of imperative code.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Who Is This For?
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
**Library authors** building on TypeScript — frontend or backend — who need a solid reactive foundation. The library is designed so that consuming libraries do not have to implement their own reactive primitives. Patterns like external data feeds, async derivations, and keyed collections are handled correctly within a unified graph rather than bolted on as ad-hoc extensions.
|
|
12
12
|
|
|
13
|
-
|
|
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()`)
|
|
13
|
+
**Experienced developers** who want to write framework-agnostic applications with explicit dependencies, predictable updates, and type safety. If you are comfortable composing your own rendering and application layers on top of reactive primitives, this library gives you the guarantees without the opinions.
|
|
21
14
|
|
|
22
|
-
|
|
15
|
+
Cause & Effect is open source, built to power **Le Truc** (a Web Component library) by [Zeix AG](https://zeix.com).
|
|
23
16
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
17
|
+
## Signal Types
|
|
18
|
+
|
|
19
|
+
Every signal type participates in the same dependency graph with the same propagation, batching, and cleanup semantics. Each type is justified by a distinct role in the graph and the data structure it manages:
|
|
20
|
+
|
|
21
|
+
| Type | Role | Create with |
|
|
22
|
+
|------|------|-------------|
|
|
23
|
+
| **State** | Mutable source | `createState()` |
|
|
24
|
+
| **Sensor** | External input source (lazy lifecycle) | `createSensor()` |
|
|
25
|
+
| **Memo** | Synchronous derivation (memoized) | `createMemo()` |
|
|
26
|
+
| **Task** | Asynchronous derivation (memoized, cancellable) | `createTask()` |
|
|
27
|
+
| **Store** | Reactive object (keyed properties, proxy-based) | `createStore()` |
|
|
28
|
+
| **List** | Reactive array (keyed items, stable identity) | `createList()` |
|
|
29
|
+
| **Collection** | Reactive collection (external source or derived, item-level memoization) | `createCollection()` |
|
|
30
|
+
| **Effect** | Side-effect sink (terminal) | `createEffect()` |
|
|
31
|
+
|
|
32
|
+
## Design Principles
|
|
33
|
+
|
|
34
|
+
- **Explicit reactivity**: Dependencies are tracked through `.get()` calls — the graph always reflects the true dependency structure, with no hidden subscriptions
|
|
35
|
+
- **Non-nullable types**: All signals enforce `T extends {}`, excluding `null` and `undefined` at the type level — you can trust returned values without null checks
|
|
36
|
+
- **Unified graph**: Composite signals (Store, List, Collection) and async signals (Task) are first-class citizens, not afterthoughts — all derivable state can be derived
|
|
37
|
+
- **Tree-shakable, zero dependencies**: Import only what you use — core signals (State, Memo, Task, Effect) stay below 5 kB gzipped, the full library below 10 kB
|
|
30
38
|
|
|
31
39
|
## Installation
|
|
32
40
|
|
|
@@ -41,13 +49,13 @@ bun add @zeix/cause-effect
|
|
|
41
49
|
## Quick Start
|
|
42
50
|
|
|
43
51
|
```js
|
|
44
|
-
import {
|
|
52
|
+
import { createState, createMemo, createEffect } from '@zeix/cause-effect'
|
|
45
53
|
|
|
46
54
|
// 1. Create state
|
|
47
|
-
const user =
|
|
55
|
+
const user = createState({ name: 'Alice', age: 30 })
|
|
48
56
|
|
|
49
57
|
// 2. Create computed values
|
|
50
|
-
const greeting =
|
|
58
|
+
const greeting = createMemo(() => `Hello ${user.get().name}!`)
|
|
51
59
|
|
|
52
60
|
// 3. React to changes
|
|
53
61
|
createEffect(() => {
|
|
@@ -58,16 +66,16 @@ createEffect(() => {
|
|
|
58
66
|
user.update(u => ({ ...u, age: 31 })) // Logs: "Hello Alice! You are 31 years old"
|
|
59
67
|
```
|
|
60
68
|
|
|
61
|
-
##
|
|
69
|
+
## API
|
|
62
70
|
|
|
63
71
|
### State
|
|
64
72
|
|
|
65
|
-
A
|
|
73
|
+
A mutable source signal. Every signal has a `.get()` method to read its current value. State signals also provide `.set()` to assign a new value and `.update()` to modify it with a function.
|
|
66
74
|
|
|
67
75
|
```js
|
|
68
|
-
import {
|
|
76
|
+
import { createState, createEffect } from '@zeix/cause-effect'
|
|
69
77
|
|
|
70
|
-
const count =
|
|
78
|
+
const count = createState(42)
|
|
71
79
|
|
|
72
80
|
createEffect(() => console.log(count.get()))
|
|
73
81
|
count.set(24)
|
|
@@ -77,17 +85,54 @@ document.querySelector('.increment').addEventListener('click', () => {
|
|
|
77
85
|
})
|
|
78
86
|
```
|
|
79
87
|
|
|
80
|
-
Use
|
|
88
|
+
Use State for primitives or for objects you replace entirely.
|
|
89
|
+
|
|
90
|
+
### Sensor
|
|
91
|
+
|
|
92
|
+
A read-only source that tracks external input. It activates lazily when first accessed by an effect and cleans up when no effects are watching:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
import { createSensor, createEffect } from '@zeix/cause-effect'
|
|
96
|
+
|
|
97
|
+
const mousePos = createSensor((set) => {
|
|
98
|
+
const handler = (e) => set({ x: e.clientX, y: e.clientY })
|
|
99
|
+
window.addEventListener('mousemove', handler)
|
|
100
|
+
return () => window.removeEventListener('mousemove', handler)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
createEffect(() => {
|
|
104
|
+
const pos = mousePos.get()
|
|
105
|
+
if (pos) console.log(`Mouse: ${pos.x}, ${pos.y}`)
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Use Sensor for mouse position, window size, media queries, geolocation, device orientation, or any external value stream.
|
|
110
|
+
|
|
111
|
+
**Observing mutable objects**: Use `SKIP_EQUALITY` when the reference stays the same but internal state changes:
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
import { createSensor, SKIP_EQUALITY, createEffect } from '@zeix/cause-effect'
|
|
115
|
+
|
|
116
|
+
const el = document.getElementById('status')
|
|
117
|
+
const element = createSensor((set) => {
|
|
118
|
+
set(el)
|
|
119
|
+
const observer = new MutationObserver(() => set(el))
|
|
120
|
+
observer.observe(el, { attributes: true, childList: true })
|
|
121
|
+
return () => observer.disconnect()
|
|
122
|
+
}, { value: el, equals: SKIP_EQUALITY })
|
|
123
|
+
|
|
124
|
+
createEffect(() => console.log(element.get().className))
|
|
125
|
+
```
|
|
81
126
|
|
|
82
127
|
### Memo
|
|
83
128
|
|
|
84
|
-
A
|
|
129
|
+
A memoized read-only derivation. It automatically tracks dependencies and updates only when those dependencies actually change.
|
|
85
130
|
|
|
86
131
|
```js
|
|
87
|
-
import {
|
|
132
|
+
import { createState, createMemo, createEffect } from '@zeix/cause-effect'
|
|
88
133
|
|
|
89
|
-
const count =
|
|
90
|
-
const isEven =
|
|
134
|
+
const count = createState(42)
|
|
135
|
+
const isEven = createMemo(() => !(count.get() % 2))
|
|
91
136
|
|
|
92
137
|
createEffect(() => console.log(isEven.get()))
|
|
93
138
|
count.set(24) // no log; still even
|
|
@@ -99,32 +144,32 @@ count.set(24) // no log; still even
|
|
|
99
144
|
const isEven = () => !(count.get() % 2)
|
|
100
145
|
```
|
|
101
146
|
|
|
102
|
-
**Advanced**: Reducer-style memos:
|
|
147
|
+
**Advanced**: Reducer-style memos with previous value access:
|
|
103
148
|
|
|
104
149
|
```js
|
|
105
|
-
import {
|
|
150
|
+
import { createState, createMemo } from '@zeix/cause-effect'
|
|
106
151
|
|
|
107
|
-
const actions =
|
|
108
|
-
const counter =
|
|
152
|
+
const actions = createState('reset')
|
|
153
|
+
const counter = createMemo(prev => {
|
|
109
154
|
switch (actions.get()) {
|
|
110
155
|
case 'increment': return prev + 1
|
|
111
156
|
case 'decrement': return prev - 1
|
|
112
157
|
case 'reset': return 0
|
|
113
158
|
default: return prev
|
|
114
159
|
}
|
|
115
|
-
}, 0)
|
|
160
|
+
}, { value: 0 })
|
|
116
161
|
```
|
|
117
162
|
|
|
118
163
|
### Task
|
|
119
164
|
|
|
120
|
-
|
|
165
|
+
An asynchronous derivation with automatic cancellation. When dependencies change while a computation is in flight, the previous one is aborted:
|
|
121
166
|
|
|
122
167
|
```js
|
|
123
|
-
import {
|
|
168
|
+
import { createState, createTask } from '@zeix/cause-effect'
|
|
124
169
|
|
|
125
|
-
const id =
|
|
170
|
+
const id = createState(1)
|
|
126
171
|
|
|
127
|
-
const data =
|
|
172
|
+
const data = createTask(async (oldValue, abort) => {
|
|
128
173
|
const response = await fetch(`/api/users/${id.get()}`, { signal: abort })
|
|
129
174
|
if (!response.ok) throw new Error('Failed to fetch')
|
|
130
175
|
return response.json()
|
|
@@ -133,11 +178,13 @@ const data = new Task(async (oldValue, abort) => {
|
|
|
133
178
|
id.set(2) // cancels previous fetch automatically
|
|
134
179
|
```
|
|
135
180
|
|
|
136
|
-
|
|
181
|
+
Tasks also provide `.isPending()` to check if a computation is in progress and `.abort()` to manually cancel.
|
|
182
|
+
|
|
183
|
+
Use Task (not plain async functions) when you need memoization, cancellation, and reactive pending/error states.
|
|
137
184
|
|
|
138
185
|
### Store
|
|
139
186
|
|
|
140
|
-
A
|
|
187
|
+
A reactive object where each property becomes its own signal. Nested objects recursively become nested stores. A Proxy provides direct property access:
|
|
141
188
|
|
|
142
189
|
```js
|
|
143
190
|
import { createStore, createEffect } from '@zeix/cause-effect'
|
|
@@ -159,121 +206,130 @@ user.preferences.theme.set('light')
|
|
|
159
206
|
createEffect(() => console.log('User:', user.get()))
|
|
160
207
|
```
|
|
161
208
|
|
|
162
|
-
|
|
209
|
+
Iterate keys using the reactive `.keys()` method to observe structural changes:
|
|
163
210
|
|
|
164
211
|
```js
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
settings.remove('timeout')
|
|
212
|
+
for (const key of user.keys()) {
|
|
213
|
+
console.log(key)
|
|
214
|
+
}
|
|
169
215
|
```
|
|
170
216
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
```js
|
|
174
|
-
const user = createStore({ name: 'Alice', age: 30 })
|
|
175
|
-
|
|
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))
|
|
179
|
-
|
|
180
|
-
// These will trigger the respective hooks:
|
|
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']"
|
|
184
|
-
```
|
|
217
|
+
Access properties by key using `.byKey()` or via direct property access like `user.name` (enabled by the Proxy).
|
|
185
218
|
|
|
186
|
-
|
|
219
|
+
Dynamic properties with `.add()` and `.remove()`:
|
|
187
220
|
|
|
188
221
|
```js
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
222
|
+
const settings = createStore({ autoSave: true })
|
|
223
|
+
|
|
224
|
+
settings.add('timeout', 5000)
|
|
225
|
+
settings.remove('timeout')
|
|
192
226
|
```
|
|
193
227
|
|
|
194
228
|
### List
|
|
195
229
|
|
|
196
|
-
A
|
|
230
|
+
A reactive array with individually reactive items and stable keys. Each item becomes its own signal while maintaining persistent identity through sorting and reordering:
|
|
197
231
|
|
|
198
232
|
```js
|
|
199
|
-
import {
|
|
233
|
+
import { createList, createEffect } from '@zeix/cause-effect'
|
|
200
234
|
|
|
201
|
-
const items =
|
|
235
|
+
const items = createList(['banana', 'apple', 'cherry'])
|
|
202
236
|
|
|
203
|
-
createEffect(() => console.log(`First: ${items
|
|
237
|
+
createEffect(() => console.log(`First: ${items.at(0)?.get()}`))
|
|
204
238
|
|
|
205
239
|
items.add('date')
|
|
206
240
|
items.splice(1, 1, 'orange')
|
|
207
241
|
items.sort()
|
|
208
242
|
```
|
|
209
243
|
|
|
210
|
-
|
|
244
|
+
Access items by key using `.byKey()` or by index using `.at()`. `.indexOfKey()` returns the current index of an item in the list, while `.keyAt()` returns the key of an item at a given position.
|
|
245
|
+
|
|
246
|
+
Keys are stable across reordering. Use `keyConfig` in options to control key generation:
|
|
211
247
|
|
|
212
248
|
```js
|
|
213
|
-
|
|
214
|
-
const
|
|
249
|
+
// String prefix keys
|
|
250
|
+
const items = createList(['banana', 'apple'], { keyConfig: 'item-' })
|
|
251
|
+
// Creates keys: 'item-0', 'item-1'
|
|
252
|
+
|
|
253
|
+
// Function-based keys
|
|
254
|
+
const users = createList(
|
|
255
|
+
[{ id: 'alice', name: 'Alice' }],
|
|
256
|
+
{ keyConfig: user => user.id }
|
|
257
|
+
)
|
|
215
258
|
|
|
259
|
+
const key = items.add('orange')
|
|
216
260
|
items.sort()
|
|
217
|
-
console.log(items.byKey(key))
|
|
218
|
-
console.log(items.indexOfKey(key))
|
|
261
|
+
console.log(items.byKey(key)?.get()) // 'orange'
|
|
262
|
+
console.log(items.indexOfKey(key)) // current index
|
|
219
263
|
```
|
|
220
264
|
|
|
221
|
-
Lists have `.
|
|
265
|
+
Lists have `.keys()`, `.add()`, and `.remove()` methods like stores. Additionally, they have `.sort()`, `.splice()`, and a reactive `.length` property. But unlike stores, deeply nested properties in items are not converted to individual signals.
|
|
222
266
|
|
|
223
267
|
### Collection
|
|
224
268
|
|
|
225
|
-
A
|
|
269
|
+
A reactive collection with item-level memoization. Collections can be externally-driven (via a start callback) or derived from a List or another Collection.
|
|
270
|
+
|
|
271
|
+
**Externally-driven collections** receive data from external sources (WebSocket, Server-Sent Events, etc.) via `applyChanges()`:
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
import { createCollection, createEffect } from '@zeix/cause-effect'
|
|
275
|
+
|
|
276
|
+
const items = createCollection((applyChanges) => {
|
|
277
|
+
const ws = new WebSocket('/items')
|
|
278
|
+
ws.onmessage = (e) => {
|
|
279
|
+
const { add, change, remove } = JSON.parse(e.data)
|
|
280
|
+
applyChanges({ changed: true, add, change, remove })
|
|
281
|
+
}
|
|
282
|
+
return () => ws.close()
|
|
283
|
+
}, { keyConfig: item => item.id })
|
|
284
|
+
|
|
285
|
+
createEffect(() => console.log('Items:', items.get()))
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The start callback activates lazily when the collection is first accessed by an effect and cleans up when no effects are watching. Options include `value` for initial items (default `[]`) and `keyConfig` for key generation.
|
|
289
|
+
|
|
290
|
+
**Derived collections** transform Lists or other Collections via `.deriveCollection()`:
|
|
226
291
|
|
|
227
292
|
```js
|
|
228
|
-
import {
|
|
293
|
+
import { createList } from '@zeix/cause-effect'
|
|
229
294
|
|
|
230
|
-
const users =
|
|
295
|
+
const users = createList([
|
|
231
296
|
{ id: 1, name: 'Alice', role: 'admin' },
|
|
232
297
|
{ id: 2, name: 'Bob', role: 'user' }
|
|
233
|
-
])
|
|
298
|
+
], { keyConfig: u => String(u.id) })
|
|
299
|
+
|
|
234
300
|
const profiles = users.deriveCollection(user => ({
|
|
235
301
|
...user,
|
|
236
302
|
displayName: `${user.name} (${user.role})`
|
|
237
303
|
}))
|
|
238
304
|
|
|
239
|
-
|
|
240
|
-
console.log(userProfiles.at(0).get().displayName)
|
|
305
|
+
console.log(profiles.at(0)?.get().displayName)
|
|
241
306
|
```
|
|
242
307
|
|
|
243
|
-
Async mapping is supported:
|
|
308
|
+
Async mapping is supported:
|
|
244
309
|
|
|
245
310
|
```js
|
|
246
|
-
const details = users.
|
|
311
|
+
const details = users.deriveCollection(async (user, abort) => {
|
|
247
312
|
const response = await fetch(`/users/${user.id}`, { signal: abort })
|
|
248
313
|
return { ...user, details: await response.json() }
|
|
249
314
|
})
|
|
250
315
|
```
|
|
251
316
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
A `Ref` is a signal that holds a reference to an external object that can change outside the reactive system.
|
|
317
|
+
Collections can be chained for data pipelines:
|
|
255
318
|
|
|
256
319
|
```js
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
createEffect(() => console.log(elementRef.get().className))
|
|
262
|
-
|
|
263
|
-
// external mutation happened
|
|
264
|
-
elementRef.notify()
|
|
320
|
+
const processed = users
|
|
321
|
+
.deriveCollection(user => ({ ...user, active: user.lastLogin > threshold }))
|
|
322
|
+
.deriveCollection(user => user.active ? `Active: ${user.name}` : `Inactive: ${user.name}`)
|
|
265
323
|
```
|
|
266
324
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
## Effects
|
|
325
|
+
### Effect
|
|
270
326
|
|
|
271
|
-
|
|
327
|
+
A side-effect sink that runs whenever the signals it reads change. Effects are terminal — they consume values but produce none. The returned function disposes the effect:
|
|
272
328
|
|
|
273
329
|
```js
|
|
274
|
-
import {
|
|
330
|
+
import { createState, createEffect } from '@zeix/cause-effect'
|
|
275
331
|
|
|
276
|
-
const count =
|
|
332
|
+
const count = createState(42)
|
|
277
333
|
|
|
278
334
|
const cleanup = createEffect(() => {
|
|
279
335
|
console.log(count.get())
|
|
@@ -283,81 +339,84 @@ const cleanup = createEffect(() => {
|
|
|
283
339
|
cleanup()
|
|
284
340
|
```
|
|
285
341
|
|
|
286
|
-
|
|
342
|
+
Effect callbacks can return a cleanup function that runs before the effect re-runs or when disposed:
|
|
287
343
|
|
|
288
344
|
```js
|
|
289
|
-
createEffect(
|
|
290
|
-
const
|
|
291
|
-
|
|
345
|
+
createEffect(() => {
|
|
346
|
+
const timer = setInterval(() => console.log(count.get()), 1000)
|
|
347
|
+
return () => clearInterval(timer)
|
|
292
348
|
})
|
|
293
349
|
```
|
|
294
350
|
|
|
295
|
-
|
|
351
|
+
#### Error Handling: match()
|
|
296
352
|
|
|
297
|
-
Use `
|
|
353
|
+
Use `match()` inside effects to handle signal values declaratively, including pending and error states from Tasks:
|
|
298
354
|
|
|
299
355
|
```js
|
|
300
|
-
import {
|
|
356
|
+
import { createState, createTask, createEffect, match } from '@zeix/cause-effect'
|
|
301
357
|
|
|
302
|
-
const userId =
|
|
303
|
-
const userData =
|
|
358
|
+
const userId = createState(1)
|
|
359
|
+
const userData = createTask(async (_, abort) => {
|
|
304
360
|
const res = await fetch(`/api/users/${userId.get()}`, { signal: abort })
|
|
305
361
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
306
362
|
return res.json()
|
|
307
363
|
})
|
|
308
364
|
|
|
309
365
|
createEffect(() => {
|
|
310
|
-
match(
|
|
311
|
-
ok: (
|
|
366
|
+
match([userData], {
|
|
367
|
+
ok: ([user]) => console.log('User:', user),
|
|
312
368
|
nil: () => console.log('Loading...'),
|
|
313
369
|
err: errors => console.error(errors[0])
|
|
314
370
|
})
|
|
315
371
|
})
|
|
316
372
|
```
|
|
317
373
|
|
|
318
|
-
##
|
|
374
|
+
## Choosing the Right Signal
|
|
319
375
|
|
|
320
376
|
```
|
|
321
|
-
|
|
377
|
+
Does the data come from *outside* the reactive system?
|
|
378
|
+
│
|
|
379
|
+
├─ Yes, single value → `createSensor(set => { ... })`
|
|
380
|
+
│ (mouse position, window resize, media queries, DOM observers, etc.)
|
|
381
|
+
│ Tip: Use `{ equals: SKIP_EQUALITY }` for mutable object observation
|
|
322
382
|
│
|
|
323
|
-
├─
|
|
324
|
-
│
|
|
325
|
-
│ Remember: call `.notify()` when it changes externally.
|
|
383
|
+
├─ Yes, keyed collection → `createCollection(applyChanges => { ... })`
|
|
384
|
+
│ (WebSocket streams, Server-Sent Events, external data feeds, etc.)
|
|
326
385
|
│
|
|
327
|
-
└─
|
|
386
|
+
└─ No, managed internally? What kind of data is it?
|
|
328
387
|
│
|
|
329
388
|
├─ *Primitive* (number/string/boolean)
|
|
330
389
|
│ │
|
|
331
390
|
│ ├─ Do you want to mutate it directly?
|
|
332
|
-
│ │ └─ Yes → `
|
|
391
|
+
│ │ └─ Yes → `createState()`
|
|
333
392
|
│ │
|
|
334
393
|
│ └─ Is it derived from other signals?
|
|
335
394
|
│ │
|
|
336
395
|
│ ├─ Sync derived
|
|
337
396
|
│ │ ├─ Simple/cheap → plain function (preferred)
|
|
338
|
-
│ │ └─ Expensive/shared/stateful → `
|
|
339
|
-
│ │
|
|
340
|
-
│ └─ Async derived → `
|
|
397
|
+
│ │ └─ Expensive/shared/stateful → `createMemo()`
|
|
398
|
+
│ │
|
|
399
|
+
│ └─ Async derived → `createTask()`
|
|
341
400
|
│ (cancellation + memoization + pending/error state)
|
|
342
401
|
│
|
|
343
402
|
├─ *Plain Object*
|
|
344
403
|
│ │
|
|
345
404
|
│ ├─ Do you want to mutate individual properties?
|
|
346
|
-
│ │ ├─ Yes → `
|
|
347
|
-
│ │ └─ No, whole object mutations only → `
|
|
405
|
+
│ │ ├─ Yes → `createStore()`
|
|
406
|
+
│ │ └─ No, whole object mutations only → `createState()`
|
|
348
407
|
│ │
|
|
349
408
|
│ └─ Is it derived from other signals?
|
|
350
|
-
│ ├─ Sync derived → plain function or `
|
|
351
|
-
│ └─ Async derived → `
|
|
409
|
+
│ ├─ Sync derived → plain function or `createMemo()`
|
|
410
|
+
│ └─ Async derived → `createTask()`
|
|
352
411
|
│
|
|
353
412
|
└─ *Array*
|
|
354
413
|
│
|
|
355
414
|
├─ Do you need to mutate it (add/remove/sort) with stable item identity?
|
|
356
|
-
│ ├─ Yes → `
|
|
357
|
-
│ └─ No, whole array mutations only → `
|
|
415
|
+
│ ├─ Yes → `createList()`
|
|
416
|
+
│ └─ No, whole array mutations only → `createState()`
|
|
358
417
|
│
|
|
359
418
|
└─ Is it derived / read-only transformation of a `List` or `Collection`?
|
|
360
|
-
└─ Yes → `
|
|
419
|
+
└─ Yes → `.deriveCollection()`
|
|
361
420
|
(memoized + supports async mapping + chaining)
|
|
362
421
|
```
|
|
363
422
|
|
|
@@ -368,12 +427,12 @@ Is the value managed *inside* the reactive system?
|
|
|
368
427
|
Group multiple signal updates, ensuring effects run only once after all changes are applied:
|
|
369
428
|
|
|
370
429
|
```js
|
|
371
|
-
import {
|
|
430
|
+
import { batch, createState } from '@zeix/cause-effect'
|
|
372
431
|
|
|
373
|
-
const a =
|
|
374
|
-
const b =
|
|
432
|
+
const a = createState(2)
|
|
433
|
+
const b = createState(3)
|
|
375
434
|
|
|
376
|
-
|
|
435
|
+
batch(() => {
|
|
377
436
|
a.set(4)
|
|
378
437
|
b.set(5)
|
|
379
438
|
})
|
|
@@ -381,16 +440,16 @@ batchSignalWrites(() => {
|
|
|
381
440
|
|
|
382
441
|
### Cleanup
|
|
383
442
|
|
|
384
|
-
Effects return a cleanup function. When executed, it will unsubscribe from signals and run cleanup functions returned by effect callbacks
|
|
443
|
+
Effects return a cleanup function. When executed, it will unsubscribe from signals and run cleanup functions returned by effect callbacks.
|
|
385
444
|
|
|
386
445
|
```js
|
|
387
|
-
import {
|
|
446
|
+
import { createState, createEffect } from '@zeix/cause-effect'
|
|
388
447
|
|
|
389
|
-
const user =
|
|
448
|
+
const user = createState({ name: 'Alice', age: 30 })
|
|
390
449
|
const greeting = () => `Hello ${user.get().name}!`
|
|
391
450
|
const cleanup = createEffect(() => {
|
|
392
|
-
|
|
393
|
-
|
|
451
|
+
console.log(`${greeting()} You are ${user.get().age} years old`)
|
|
452
|
+
return () => console.log('Cleanup')
|
|
394
453
|
})
|
|
395
454
|
|
|
396
455
|
// When you no longer need the effect, execute the cleanup function
|
|
@@ -399,109 +458,71 @@ cleanup() // Logs: 'Cleanup' and unsubscribes from signal `user`
|
|
|
399
458
|
user.set({ name: 'Bob', age: 28 }) // Won't trigger the effect anymore
|
|
400
459
|
```
|
|
401
460
|
|
|
402
|
-
###
|
|
461
|
+
### Scoped Cleanup
|
|
403
462
|
|
|
404
|
-
|
|
463
|
+
Use `createScope()` for hierarchical cleanup of nested effects and resources. It returns a single cleanup function:
|
|
405
464
|
|
|
406
465
|
```js
|
|
407
|
-
import {
|
|
408
|
-
|
|
409
|
-
const config = new State({ apiUrl: 'https://api.example.com' })
|
|
410
|
-
|
|
411
|
-
// Set up lazy resource management
|
|
412
|
-
config.on('watch', () => {
|
|
413
|
-
console.log('Setting up API client...')
|
|
414
|
-
const client = new ApiClient(config.get().apiUrl)
|
|
415
|
-
|
|
416
|
-
// Return cleanup function
|
|
417
|
-
return () => {
|
|
418
|
-
console.log('Cleaning up API client...')
|
|
419
|
-
client.disconnect()
|
|
420
|
-
}
|
|
421
|
-
})
|
|
466
|
+
import { createState, createEffect, createScope } from '@zeix/cause-effect'
|
|
422
467
|
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
console.log(
|
|
468
|
+
const dispose = createScope(() => {
|
|
469
|
+
const count = createState(0)
|
|
470
|
+
createEffect(() => console.log(count.get()))
|
|
471
|
+
return () => console.log('Scope disposed')
|
|
426
472
|
})
|
|
427
473
|
|
|
428
|
-
//
|
|
429
|
-
cleanup()
|
|
474
|
+
dispose() // Cleans up the effect and runs the returned cleanup
|
|
430
475
|
```
|
|
431
476
|
|
|
432
|
-
|
|
433
|
-
- Event listeners that should only be active when data is being watched
|
|
434
|
-
- Network connections that can be lazily established
|
|
435
|
-
- Expensive computations that should pause when not needed
|
|
436
|
-
- External subscriptions (WebSocket, Server-Sent Events, etc.)
|
|
437
|
-
|
|
438
|
-
### resolve()
|
|
477
|
+
### Resource Management with Watch Callbacks
|
|
439
478
|
|
|
440
|
-
|
|
479
|
+
Sensor and Collection signals use a **start callback** for lazy resource management. The callback runs when the signal is first accessed by an effect and the returned cleanup function runs when no effects are watching:
|
|
441
480
|
|
|
442
481
|
```js
|
|
443
|
-
import {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
else console.error('Errors:', result.errors)
|
|
452
|
-
```
|
|
453
|
-
|
|
454
|
-
### match()
|
|
455
|
-
|
|
456
|
-
Pattern matching on resolved results for side effects:
|
|
457
|
-
|
|
458
|
-
```js
|
|
459
|
-
import { resolve, match } from '@zeix/cause-effect'
|
|
460
|
-
|
|
461
|
-
match(resolve({ name, age }), {
|
|
462
|
-
ok: ({ name, age }) => document.title = `${name} (${age})`,
|
|
463
|
-
nil: () => document.title = 'Loading...',
|
|
464
|
-
err: errors => document.title = `Error: ${errors[0].message}`
|
|
482
|
+
import { createSensor, createCollection, createEffect } from '@zeix/cause-effect'
|
|
483
|
+
|
|
484
|
+
// Sensor: track external input
|
|
485
|
+
const windowSize = createSensor((set) => {
|
|
486
|
+
const update = () => set({ w: innerWidth, h: innerHeight })
|
|
487
|
+
update()
|
|
488
|
+
window.addEventListener('resize', update)
|
|
489
|
+
return () => window.removeEventListener('resize', update)
|
|
465
490
|
})
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
### diff()
|
|
469
491
|
|
|
470
|
-
|
|
492
|
+
// Collection: receive external data
|
|
493
|
+
const feed = createCollection((applyChanges) => {
|
|
494
|
+
const es = new EventSource('/feed')
|
|
495
|
+
es.onmessage = (e) => applyChanges(JSON.parse(e.data))
|
|
496
|
+
return () => es.close()
|
|
497
|
+
}, { keyConfig: item => item.id })
|
|
471
498
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
499
|
+
// Resources are created only when effect runs
|
|
500
|
+
const cleanup = createEffect(() => {
|
|
501
|
+
console.log('Window size:', windowSize.get())
|
|
502
|
+
console.log('Feed items:', feed.get())
|
|
503
|
+
})
|
|
477
504
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
console.log(changes.add) // { email: 'alice@example.com' }
|
|
481
|
-
console.log(changes.change) // { age: 31 }
|
|
482
|
-
console.log(changes.remove) // { city: UNSET }
|
|
505
|
+
// Resources are cleaned up when effect stops
|
|
506
|
+
cleanup()
|
|
483
507
|
```
|
|
484
508
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
Deep equality comparison with circular reference detection:
|
|
509
|
+
Store and List signals support an optional `watched` callback in their options that returns a cleanup function:
|
|
488
510
|
|
|
489
511
|
```js
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
console.log(isEqual(obj1, obj2)) // true - deep equality
|
|
497
|
-
console.log(isEqual(obj1, obj3)) // false - names differ
|
|
498
|
-
|
|
499
|
-
// Handles arrays, primitives, and complex nested structures
|
|
500
|
-
console.log(isEqual([1, 2, 3], [1, 2, 3])) // true
|
|
501
|
-
console.log(isEqual('hello', 'hello')) // true
|
|
502
|
-
console.log(isEqual({ a: [1, 2] }, { a: [1, 2] })) // true
|
|
512
|
+
const user = createStore({ name: 'Alice' }, {
|
|
513
|
+
watched: () => {
|
|
514
|
+
const ws = new WebSocket('/updates')
|
|
515
|
+
return () => ws.close()
|
|
516
|
+
}
|
|
517
|
+
})
|
|
503
518
|
```
|
|
504
519
|
|
|
520
|
+
This pattern is ideal for:
|
|
521
|
+
- Event listeners that should only be active when data is being watched
|
|
522
|
+
- Network connections that can be lazily established
|
|
523
|
+
- Expensive computations that should pause when not needed
|
|
524
|
+
- External subscriptions (WebSocket, Server-Sent Events, etc.)
|
|
525
|
+
|
|
505
526
|
## Contributing & License
|
|
506
527
|
|
|
507
528
|
Feel free to contribute, report issues, or suggest improvements.
|