@rlabs-inc/signals 1.8.2 → 1.10.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/README.md +475 -360
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +109 -2
- package/dist/index.mjs +109 -2
- package/dist/primitives/effect.d.ts +1 -0
- package/dist/primitives/effect.d.ts.map +1 -1
- package/dist/primitives/tracked-slot.d.ts +43 -0
- package/dist/primitives/tracked-slot.d.ts.map +1 -0
- package/dist/reactivity/scheduling.d.ts.map +1 -1
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -2,16 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
**Production-grade fine-grained reactivity for TypeScript.**
|
|
4
4
|
|
|
5
|
-
A complete standalone
|
|
5
|
+
A complete standalone implementation of modern reactivity patterns, combining the best of Svelte 5, Angular, Solid, and Vue. No compiler needed, no DOM dependencies - works anywhere: Bun, Node, Deno, or browser.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Features
|
|
8
8
|
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
9
|
+
- **Fine-Grained Reactivity** - Changes only trigger effects that depend on them
|
|
10
|
+
- **Deep Reactivity** - Proxy-based tracking at any nesting depth
|
|
11
|
+
- **Zero Dependencies** - Pure TypeScript, ~8KB minified
|
|
12
|
+
- **Framework Patterns** - linkedSignal (Angular), createSelector (Solid), effectScope (Vue)
|
|
13
|
+
- **Sync & Async Effects** - Choose predictable timing or automatic batching
|
|
14
|
+
- **Automatic Cleanup** - FinalizationRegistry ensures proper garbage collection
|
|
15
|
+
|
|
16
|
+
## Performance
|
|
17
|
+
|
|
18
|
+
Benchmarked against Preact Signals (the fastest mainstream implementation):
|
|
19
|
+
|
|
20
|
+
| Operation | @rlabs-inc/signals | Preact Signals |
|
|
21
|
+
|-----------|-------------------|----------------|
|
|
22
|
+
| Signal read | **2ns** | 3ns |
|
|
23
|
+
| Signal write | 15ns | 12ns |
|
|
24
|
+
| Effect run | **20ns** | 25ns |
|
|
25
|
+
| Derived read | 5ns | 5ns |
|
|
26
|
+
|
|
27
|
+
Performance is competitive while offering significantly more features.
|
|
15
28
|
|
|
16
29
|
## Installation
|
|
17
30
|
|
|
@@ -26,18 +39,18 @@ npm install @rlabs-inc/signals
|
|
|
26
39
|
```typescript
|
|
27
40
|
import { signal, derived, effect, flushSync } from '@rlabs-inc/signals'
|
|
28
41
|
|
|
29
|
-
// Create a signal
|
|
42
|
+
// Create a signal (reactive value)
|
|
30
43
|
const count = signal(0)
|
|
31
44
|
|
|
32
|
-
// Create a derived value
|
|
45
|
+
// Create a derived (computed value)
|
|
33
46
|
const doubled = derived(() => count.value * 2)
|
|
34
47
|
|
|
35
|
-
// Create an effect
|
|
48
|
+
// Create an effect (side effect)
|
|
36
49
|
effect(() => {
|
|
37
50
|
console.log(`Count: ${count.value}, Doubled: ${doubled.value}`)
|
|
38
51
|
})
|
|
39
52
|
|
|
40
|
-
// Flush to run effects synchronously
|
|
53
|
+
// Flush to run effects synchronously (for testing/demos)
|
|
41
54
|
flushSync() // Logs: "Count: 0, Doubled: 0"
|
|
42
55
|
|
|
43
56
|
// Update the signal
|
|
@@ -47,6 +60,37 @@ flushSync() // Logs: "Count: 5, Doubled: 10"
|
|
|
47
60
|
|
|
48
61
|
---
|
|
49
62
|
|
|
63
|
+
## Table of Contents
|
|
64
|
+
|
|
65
|
+
- [Core Primitives](#core-primitives)
|
|
66
|
+
- [signal](#signal)
|
|
67
|
+
- [signals](#signals)
|
|
68
|
+
- [derived](#derived)
|
|
69
|
+
- [state](#state)
|
|
70
|
+
- [stateRaw](#stateraw)
|
|
71
|
+
- [Effects](#effects)
|
|
72
|
+
- [effect()](#effect-async)
|
|
73
|
+
- [effect.sync()](#effectsync)
|
|
74
|
+
- [effect.root()](#effectroot)
|
|
75
|
+
- [effect.tracking()](#effecttracking)
|
|
76
|
+
- [Advanced Primitives](#advanced-primitives)
|
|
77
|
+
- [linkedSignal](#linkedsignal-angulars-killer-feature)
|
|
78
|
+
- [createSelector](#createselector-solids-on-to-o2-optimization)
|
|
79
|
+
- [effectScope](#effectscope-vues-lifecycle-management)
|
|
80
|
+
- [Bindings & Slots](#bindings--slots)
|
|
81
|
+
- [bind](#bind)
|
|
82
|
+
- [slot](#slot)
|
|
83
|
+
- [slotArray](#slotarray)
|
|
84
|
+
- [trackedSlotArray](#trackedslotarray)
|
|
85
|
+
- [reactiveProps](#reactiveprops)
|
|
86
|
+
- [Deep Reactivity](#deep-reactivity)
|
|
87
|
+
- [Utilities](#utilities)
|
|
88
|
+
- [Reactive Collections](#reactive-collections)
|
|
89
|
+
- [Equality Functions](#equality-functions)
|
|
90
|
+
- [Error Handling](#error-handling)
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
50
94
|
## Core Primitives
|
|
51
95
|
|
|
52
96
|
### signal
|
|
@@ -60,7 +104,10 @@ signal<T>(initialValue: T, options?: { equals?: (a: T, b: T) => boolean }): Writ
|
|
|
60
104
|
```typescript
|
|
61
105
|
const name = signal('John')
|
|
62
106
|
console.log(name.value) // 'John'
|
|
63
|
-
name.value = 'Jane' // Triggers effects
|
|
107
|
+
name.value = 'Jane' // Triggers effects that depend on it
|
|
108
|
+
|
|
109
|
+
// With custom equality (skip updates when structurally equal)
|
|
110
|
+
const user = signal({ name: 'John' }, { equals: deepEquals })
|
|
64
111
|
```
|
|
65
112
|
|
|
66
113
|
### signals
|
|
@@ -83,6 +130,30 @@ ui.content.value = 'updated'
|
|
|
83
130
|
ui.width.value = 60
|
|
84
131
|
```
|
|
85
132
|
|
|
133
|
+
### derived
|
|
134
|
+
|
|
135
|
+
Create a computed value that automatically updates when dependencies change.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
derived<T>(fn: () => T, options?: { equals?: Equals<T> }): DerivedSignal<T>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
const firstName = signal('John')
|
|
143
|
+
const lastName = signal('Doe')
|
|
144
|
+
|
|
145
|
+
const fullName = derived(() => `${firstName.value} ${lastName.value}`)
|
|
146
|
+
console.log(fullName.value) // 'John Doe'
|
|
147
|
+
|
|
148
|
+
firstName.value = 'Jane'
|
|
149
|
+
console.log(fullName.value) // 'Jane Doe'
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Deriveds are:
|
|
153
|
+
- **Lazy** - Only computed when read
|
|
154
|
+
- **Cached** - Value is memoized until dependencies change
|
|
155
|
+
- **Pure** - Cannot write to signals inside (throws error)
|
|
156
|
+
|
|
86
157
|
### state
|
|
87
158
|
|
|
88
159
|
Create a deeply reactive object. No `.value` needed - access properties directly.
|
|
@@ -94,10 +165,13 @@ state<T extends object>(initialValue: T): T
|
|
|
94
165
|
```typescript
|
|
95
166
|
const user = state({
|
|
96
167
|
name: 'John',
|
|
97
|
-
address: { city: 'NYC' }
|
|
168
|
+
address: { city: 'NYC', zip: '10001' }
|
|
98
169
|
})
|
|
99
|
-
|
|
100
|
-
|
|
170
|
+
|
|
171
|
+
// All property access is reactive
|
|
172
|
+
user.name = 'Jane' // Triggers effects reading user.name
|
|
173
|
+
user.address.city = 'LA' // Triggers effects reading user.address.city
|
|
174
|
+
// Effects reading user.name are NOT triggered by city change (fine-grained!)
|
|
101
175
|
```
|
|
102
176
|
|
|
103
177
|
### stateRaw
|
|
@@ -119,34 +193,15 @@ Use `stateRaw` for:
|
|
|
119
193
|
- Class instances
|
|
120
194
|
- Large objects where you only care about replacement
|
|
121
195
|
|
|
122
|
-
### derived
|
|
123
|
-
|
|
124
|
-
Create a computed value that automatically updates when dependencies change.
|
|
125
|
-
|
|
126
|
-
```typescript
|
|
127
|
-
derived<T>(fn: () => T, options?: { equals?: Equals<T> }): DerivedSignal<T>
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
```typescript
|
|
131
|
-
const firstName = signal('John')
|
|
132
|
-
const lastName = signal('Doe')
|
|
133
|
-
|
|
134
|
-
const fullName = derived(() => `${firstName.value} ${lastName.value}`)
|
|
135
|
-
console.log(fullName.value) // 'John Doe'
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
Deriveds are:
|
|
139
|
-
- **Lazy** - Only computed when read
|
|
140
|
-
- **Cached** - Value is memoized until dependencies change
|
|
141
|
-
- **Pure** - Cannot write to signals inside (throws error)
|
|
142
|
-
|
|
143
196
|
---
|
|
144
197
|
|
|
145
198
|
## Effects
|
|
146
199
|
|
|
147
|
-
|
|
200
|
+
Effects are the bridge between reactive state and the outside world. They re-run when their dependencies change.
|
|
148
201
|
|
|
149
|
-
|
|
202
|
+
### effect() (async)
|
|
203
|
+
|
|
204
|
+
Create an effect that runs asynchronously via microtask. Multiple signal changes are automatically batched.
|
|
150
205
|
|
|
151
206
|
```typescript
|
|
152
207
|
effect(fn: () => void | CleanupFn): DisposeFn
|
|
@@ -158,41 +213,75 @@ const count = signal(0)
|
|
|
158
213
|
const dispose = effect(() => {
|
|
159
214
|
console.log('Count is:', count.value)
|
|
160
215
|
|
|
161
|
-
// Optional cleanup function
|
|
162
|
-
return () =>
|
|
163
|
-
console.log('Cleaning up...')
|
|
164
|
-
}
|
|
216
|
+
// Optional cleanup function - runs before next execution
|
|
217
|
+
return () => console.log('Cleaning up...')
|
|
165
218
|
})
|
|
166
219
|
|
|
220
|
+
count.value = 1
|
|
221
|
+
count.value = 2
|
|
222
|
+
count.value = 3
|
|
223
|
+
// Effect runs ONCE with final value (3) on next microtask
|
|
224
|
+
|
|
167
225
|
// Stop the effect
|
|
168
226
|
dispose()
|
|
169
227
|
```
|
|
170
228
|
|
|
171
|
-
|
|
229
|
+
**When to use:** Most UI work, general reactivity. The automatic batching provides better throughput.
|
|
230
|
+
|
|
231
|
+
### effect.sync()
|
|
172
232
|
|
|
173
|
-
Create
|
|
233
|
+
Create a synchronous effect that runs immediately when dependencies change. Combine with `batch()` for best performance.
|
|
174
234
|
|
|
175
235
|
```typescript
|
|
176
|
-
effect.
|
|
177
|
-
|
|
236
|
+
effect.sync(fn: () => void | CleanupFn): DisposeFn
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
const count = signal(0)
|
|
241
|
+
|
|
242
|
+
effect.sync(() => {
|
|
243
|
+
console.log('Count:', count.value)
|
|
178
244
|
})
|
|
245
|
+
// Logs: "Count: 0" (runs immediately)
|
|
246
|
+
|
|
247
|
+
count.value = 1 // Logs: "Count: 1" (runs immediately)
|
|
248
|
+
count.value = 2 // Logs: "Count: 2" (runs immediately)
|
|
249
|
+
|
|
250
|
+
// For better performance with multiple writes, use batch():
|
|
251
|
+
batch(() => {
|
|
252
|
+
count.value = 10
|
|
253
|
+
count.value = 20
|
|
254
|
+
count.value = 30
|
|
255
|
+
})
|
|
256
|
+
// Logs: "Count: 30" (runs once at end of batch)
|
|
179
257
|
```
|
|
180
258
|
|
|
181
|
-
|
|
259
|
+
**When to use:**
|
|
260
|
+
- Debugging (predictable execution order)
|
|
261
|
+
- Testing (synchronous assertions)
|
|
262
|
+
- Sequential logic where timing matters
|
|
263
|
+
- When you need immediate side effects
|
|
182
264
|
|
|
183
|
-
|
|
265
|
+
### effect.root()
|
|
266
|
+
|
|
267
|
+
Create a root effect scope that can contain nested effects.
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
effect.root(fn: () => void): DisposeFn
|
|
271
|
+
```
|
|
184
272
|
|
|
185
273
|
```typescript
|
|
186
274
|
const dispose = effect.root(() => {
|
|
187
|
-
effect(() =>
|
|
188
|
-
effect(() =>
|
|
275
|
+
effect(() => console.log('Effect A'))
|
|
276
|
+
effect(() => console.log('Effect B'))
|
|
277
|
+
effect.sync(() => console.log('Sync Effect C'))
|
|
189
278
|
})
|
|
190
279
|
|
|
191
|
-
//
|
|
280
|
+
// Later, clean up ALL nested effects at once
|
|
192
281
|
dispose()
|
|
193
282
|
```
|
|
194
283
|
|
|
195
|
-
### effect.tracking
|
|
284
|
+
### effect.tracking()
|
|
196
285
|
|
|
197
286
|
Check if currently inside a reactive tracking context.
|
|
198
287
|
|
|
@@ -202,86 +291,195 @@ if (effect.tracking()) {
|
|
|
202
291
|
}
|
|
203
292
|
```
|
|
204
293
|
|
|
205
|
-
|
|
294
|
+
### effect.pre (deprecated)
|
|
206
295
|
|
|
207
|
-
|
|
296
|
+
Alias for `effect.sync()`. Use `effect.sync()` instead for clarity.
|
|
208
297
|
|
|
209
|
-
|
|
298
|
+
---
|
|
210
299
|
|
|
211
|
-
|
|
300
|
+
## Advanced Primitives
|
|
212
301
|
|
|
213
|
-
|
|
302
|
+
### linkedSignal (Angular's killer feature)
|
|
303
|
+
|
|
304
|
+
Create a writable signal that derives from a source but can be manually overridden. When the source changes, the linked signal resets to the computed value.
|
|
214
305
|
|
|
215
306
|
```typescript
|
|
216
|
-
|
|
307
|
+
linkedSignal<D>(fn: () => D): WritableSignal<D>
|
|
308
|
+
linkedSignal<S, D>(options: LinkedSignalOptions<S, D>): WritableSignal<D>
|
|
217
309
|
```
|
|
218
310
|
|
|
311
|
+
**Simple form - dropdown selection:**
|
|
312
|
+
|
|
219
313
|
```typescript
|
|
220
|
-
const
|
|
221
|
-
const
|
|
314
|
+
const options = signal(['a', 'b', 'c'])
|
|
315
|
+
const selected = linkedSignal(() => options.value[0])
|
|
222
316
|
|
|
223
|
-
//
|
|
224
|
-
|
|
317
|
+
console.log(selected.value) // 'a'
|
|
318
|
+
selected.value = 'b' // Manual override
|
|
319
|
+
console.log(selected.value) // 'b'
|
|
225
320
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
console.log(
|
|
321
|
+
options.value = ['x', 'y'] // Source changes
|
|
322
|
+
flushSync()
|
|
323
|
+
console.log(selected.value) // 'x' (reset to first item)
|
|
229
324
|
```
|
|
230
325
|
|
|
231
|
-
**
|
|
232
|
-
- `bind(signal)` - Creates writable binding to signal
|
|
233
|
-
- `bind(binding)` - Chains bindings (both point to same source)
|
|
234
|
-
- `bind(value)` - Wraps raw value in a signal
|
|
235
|
-
- `bind(() => expr)` - Creates read-only binding from getter
|
|
326
|
+
**Advanced form - keep valid selection:**
|
|
236
327
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
328
|
+
```typescript
|
|
329
|
+
const items = signal([1, 2, 3])
|
|
330
|
+
const selectedItem = linkedSignal({
|
|
331
|
+
source: () => items.value,
|
|
332
|
+
computation: (newItems, prev) => {
|
|
333
|
+
// Keep selection if still valid
|
|
334
|
+
if (prev && newItems.includes(prev.value)) {
|
|
335
|
+
return prev.value
|
|
336
|
+
}
|
|
337
|
+
return newItems[0]
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**Form input that resets when data reloads:**
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
const user = signal({ name: 'Alice' })
|
|
346
|
+
const editName = linkedSignal(() => user.value.name)
|
|
347
|
+
|
|
348
|
+
// User types in input
|
|
349
|
+
editName.value = 'Bob'
|
|
350
|
+
console.log(editName.value) // 'Bob'
|
|
241
351
|
|
|
242
|
-
|
|
352
|
+
// When user data reloads from server
|
|
353
|
+
user.value = { name: 'Charlie' }
|
|
354
|
+
flushSync()
|
|
355
|
+
console.log(editName.value) // 'Charlie' (reset!)
|
|
356
|
+
```
|
|
243
357
|
|
|
244
|
-
|
|
358
|
+
### createSelector (Solid's O(n) to O(2) optimization)
|
|
359
|
+
|
|
360
|
+
Create a selector function for efficient list selection tracking. Instead of O(n) effects re-running, only affected items run = O(2).
|
|
245
361
|
|
|
246
362
|
```typescript
|
|
247
|
-
|
|
248
|
-
|
|
363
|
+
createSelector<T, U = T>(
|
|
364
|
+
source: () => T,
|
|
365
|
+
fn?: (key: U, value: T) => boolean
|
|
366
|
+
): SelectorFn<T, U>
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
const selectedId = signal(1)
|
|
371
|
+
const isSelected = createSelector(() => selectedId.value)
|
|
249
372
|
|
|
250
|
-
|
|
251
|
-
|
|
373
|
+
// In a list of 1000 items:
|
|
374
|
+
items.forEach(item => {
|
|
375
|
+
effect(() => {
|
|
376
|
+
// Only runs when THIS item's selection state changes!
|
|
377
|
+
if (isSelected(item.id)) {
|
|
378
|
+
highlight(item)
|
|
379
|
+
} else {
|
|
380
|
+
unhighlight(item)
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
// When selectedId changes from 1 to 2:
|
|
386
|
+
// - Only item 1's effect runs (was selected, now not)
|
|
387
|
+
// - Only item 2's effect runs (was not selected, now is)
|
|
388
|
+
// - Other 998 items' effects DON'T run!
|
|
252
389
|
```
|
|
253
390
|
|
|
254
|
-
###
|
|
391
|
+
### effectScope (Vue's lifecycle management)
|
|
392
|
+
|
|
393
|
+
Create an effect scope to group effects for batch disposal with pause/resume support.
|
|
255
394
|
|
|
256
395
|
```typescript
|
|
257
|
-
|
|
258
|
-
isBinding(maybeBinding) // true or false
|
|
396
|
+
effectScope(detached?: boolean): EffectScope
|
|
259
397
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
398
|
+
interface EffectScope {
|
|
399
|
+
readonly active: boolean
|
|
400
|
+
readonly paused: boolean
|
|
401
|
+
run<R>(fn: () => R): R | undefined
|
|
402
|
+
stop(): void
|
|
403
|
+
pause(): void
|
|
404
|
+
resume(): void
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
const scope = effectScope()
|
|
410
|
+
|
|
411
|
+
scope.run(() => {
|
|
412
|
+
effect(() => console.log(count.value))
|
|
413
|
+
effect(() => console.log(name.value))
|
|
265
414
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
415
|
+
// Register cleanup to run when scope stops
|
|
416
|
+
onScopeDispose(() => {
|
|
417
|
+
console.log('Cleaning up...')
|
|
418
|
+
})
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
// Pause execution temporarily
|
|
422
|
+
scope.pause()
|
|
423
|
+
count.value = 5 // Effect doesn't run
|
|
424
|
+
|
|
425
|
+
// Resume and run pending updates
|
|
426
|
+
scope.resume() // Now effect runs with value 5
|
|
427
|
+
|
|
428
|
+
// Later, dispose all effects at once
|
|
429
|
+
scope.stop() // Runs onScopeDispose callbacks
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**Related utilities:**
|
|
270
433
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
434
|
+
```typescript
|
|
435
|
+
// Register cleanup on current scope
|
|
436
|
+
onScopeDispose(() => clearInterval(timer))
|
|
437
|
+
|
|
438
|
+
// Get the currently active scope
|
|
439
|
+
const scope = getCurrentScope()
|
|
274
440
|
```
|
|
275
441
|
|
|
276
442
|
---
|
|
277
443
|
|
|
278
|
-
##
|
|
444
|
+
## Bindings & Slots
|
|
445
|
+
|
|
446
|
+
### bind
|
|
447
|
+
|
|
448
|
+
Create a reactive binding - a two-way pointer that forwards reads and writes to a source signal.
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
bind<T>(source: WritableSignal<T> | Binding<T> | T | (() => T)): Binding<T>
|
|
452
|
+
```
|
|
279
453
|
|
|
280
|
-
|
|
454
|
+
```typescript
|
|
455
|
+
const source = signal(0)
|
|
456
|
+
const binding = bind(source)
|
|
457
|
+
|
|
458
|
+
// Reading through binding reads from source
|
|
459
|
+
console.log(binding.value) // 0
|
|
460
|
+
|
|
461
|
+
// Writing through binding writes to source
|
|
462
|
+
binding.value = 42
|
|
463
|
+
console.log(source.value) // 42
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**Overloads:**
|
|
467
|
+
- `bind(signal)` - Creates writable binding to signal
|
|
468
|
+
- `bind(binding)` - Chains bindings (both point to same source)
|
|
469
|
+
- `bind(value)` - Wraps raw value in a signal
|
|
470
|
+
- `bind(() => expr)` - Creates read-only binding from getter
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
// Read-only binding from getter
|
|
474
|
+
const count = signal(5)
|
|
475
|
+
const doubled = bind(() => count.value * 2)
|
|
476
|
+
console.log(doubled.value) // 10
|
|
477
|
+
// doubled.value = 20 // Would throw!
|
|
478
|
+
```
|
|
281
479
|
|
|
282
480
|
### slot
|
|
283
481
|
|
|
284
|
-
Create a
|
|
482
|
+
Create a stable reactive cell that can point to different sources. Unlike `bind()`, a Slot is never replaced - you mutate its source.
|
|
285
483
|
|
|
286
484
|
```typescript
|
|
287
485
|
slot<T>(initial?: T): Slot<T>
|
|
@@ -295,16 +493,12 @@ console.log(mySlot.value) // 'hello'
|
|
|
295
493
|
mySlot.source = 'world'
|
|
296
494
|
console.log(mySlot.value) // 'world'
|
|
297
495
|
|
|
298
|
-
// Point to a signal
|
|
496
|
+
// Point to a signal (two-way binding!)
|
|
299
497
|
const name = signal('Alice')
|
|
300
498
|
mySlot.source = name
|
|
301
499
|
console.log(mySlot.value) // 'Alice'
|
|
302
|
-
|
|
303
|
-
console.log(
|
|
304
|
-
|
|
305
|
-
// Write through to signal (two-way binding!)
|
|
306
|
-
mySlot.set('Charlie')
|
|
307
|
-
console.log(name.value) // 'Charlie'
|
|
500
|
+
mySlot.set('Bob') // Writes through to signal!
|
|
501
|
+
console.log(name.value) // 'Bob'
|
|
308
502
|
|
|
309
503
|
// Point to a getter (read-only, auto-tracks)
|
|
310
504
|
const count = signal(5)
|
|
@@ -320,7 +514,7 @@ console.log(mySlot.value) // 'Count: 10'
|
|
|
320
514
|
|
|
321
515
|
### slotArray
|
|
322
516
|
|
|
323
|
-
Create an array of slots for efficient reactive arrays.
|
|
517
|
+
Create an array of slots for efficient reactive arrays (parallel array / ECS pattern).
|
|
324
518
|
|
|
325
519
|
```typescript
|
|
326
520
|
slotArray<T>(defaultValue?: T): SlotArray<T>
|
|
@@ -343,6 +537,7 @@ const rawSlot = textContent.slot(0)
|
|
|
343
537
|
```
|
|
344
538
|
|
|
345
539
|
**SlotArray methods:**
|
|
540
|
+
|
|
346
541
|
| Method | Description |
|
|
347
542
|
|--------|-------------|
|
|
348
543
|
| `arr[i]` | Read value at index (auto-unwraps, auto-tracks) |
|
|
@@ -352,46 +547,77 @@ const rawSlot = textContent.slot(0)
|
|
|
352
547
|
| `arr.ensureCapacity(n)` | Expand to at least n slots |
|
|
353
548
|
| `arr.clear(i)` | Reset slot i to default |
|
|
354
549
|
|
|
355
|
-
###
|
|
550
|
+
### trackedSlotArray
|
|
356
551
|
|
|
357
|
-
|
|
358
|
-
1. Components write sources to arrays
|
|
359
|
-
2. Pipelines read from arrays reactively
|
|
360
|
-
3. Two-way binding enables inputs
|
|
552
|
+
Create an array of slots with automatic dirty tracking - perfect for incremental computation and ECS-style architectures.
|
|
361
553
|
|
|
362
554
|
```typescript
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
textContent.setSource(index, () => `Count: ${count.value}`)
|
|
555
|
+
trackedSlotArray<T>(defaultValue?: T, dirtySet: ReactiveSet<number>): SlotArray<T>
|
|
556
|
+
```
|
|
366
557
|
|
|
367
|
-
|
|
368
|
-
|
|
558
|
+
```typescript
|
|
559
|
+
const dirty = new ReactiveSet<number>()
|
|
560
|
+
const positions = trackedSlotArray({ x: 0, y: 0 }, dirty)
|
|
561
|
+
|
|
562
|
+
// Set source - automatically marks index as dirty
|
|
563
|
+
positions.setSource(0, signal({ x: 10, y: 20 })) // dirty.has(0) === true
|
|
564
|
+
|
|
565
|
+
// setValue also marks dirty
|
|
566
|
+
positions.setValue(5, { x: 100, y: 200 }) // dirty.has(5) === true
|
|
369
567
|
|
|
370
|
-
//
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
568
|
+
// Use dirty tracking for O(1) skips
|
|
569
|
+
const layout = derived(() => {
|
|
570
|
+
if (dirty.size === 0) return cachedLayout // Nothing changed!
|
|
571
|
+
|
|
572
|
+
// Process only dirty indices
|
|
573
|
+
for (const index of dirty) {
|
|
574
|
+
updateLayout(index, positions[index])
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
dirty.clear()
|
|
578
|
+
return computedLayout
|
|
579
|
+
})
|
|
376
580
|
```
|
|
377
581
|
|
|
582
|
+
**Three-level reactivity:**
|
|
583
|
+
|
|
584
|
+
1. **Per-index tracking** - `dirty.has(5)` only subscribes to index 5
|
|
585
|
+
2. **Size tracking** - `dirty.size` tracks count of dirty indices (perfect for "anything changed?" checks)
|
|
586
|
+
3. **Version tracking** - Iterating `for (const i of dirty)` tracks structural changes
|
|
587
|
+
|
|
588
|
+
**Use cases:**
|
|
589
|
+
|
|
590
|
+
- **ECS systems** - Track which entities changed, skip unchanged ones
|
|
591
|
+
- **Layout engines** - Recompute only components with changed properties
|
|
592
|
+
- **Incremental compilation** - Track which modules changed
|
|
593
|
+
- **Dirty checking** - Skip expensive computations when nothing changed
|
|
594
|
+
|
|
595
|
+
**Comparison with slotArray:**
|
|
596
|
+
|
|
597
|
+
| Feature | slotArray | trackedSlotArray |
|
|
598
|
+
|---------|-----------|------------------|
|
|
599
|
+
| Basic functionality | ✅ | ✅ |
|
|
600
|
+
| Automatic dirty tracking | ❌ | ✅ |
|
|
601
|
+
| O(1) skip optimization | ❌ | ✅ |
|
|
602
|
+
| Drop-in replacement | - | ✅ (just add dirtySet param) |
|
|
603
|
+
|
|
378
604
|
### reactiveProps
|
|
379
605
|
|
|
380
606
|
Normalize component props to a consistent reactive interface. Accepts static values, getter functions, or signals - returns an object where every property is a DerivedSignal.
|
|
381
607
|
|
|
382
608
|
```typescript
|
|
383
|
-
reactiveProps<T>(rawProps:
|
|
609
|
+
reactiveProps<T>(rawProps: T): ReactiveProps<UnwrapPropInputs<T>>
|
|
384
610
|
```
|
|
385
611
|
|
|
386
612
|
```typescript
|
|
387
613
|
interface MyComponentProps {
|
|
388
|
-
name: string
|
|
389
|
-
count: number
|
|
390
|
-
active: boolean
|
|
614
|
+
name: PropInput<string>
|
|
615
|
+
count: PropInput<number>
|
|
616
|
+
active: PropInput<boolean>
|
|
391
617
|
}
|
|
392
618
|
|
|
393
|
-
function MyComponent(rawProps:
|
|
394
|
-
// Convert any mix of static/getter/signal props to consistent
|
|
619
|
+
function MyComponent(rawProps: MyComponentProps) {
|
|
620
|
+
// Convert any mix of static/getter/signal props to consistent interface
|
|
395
621
|
const props = reactiveProps(rawProps)
|
|
396
622
|
|
|
397
623
|
// Everything is now a DerivedSignal - consistent .value access
|
|
@@ -412,147 +638,104 @@ MyComponent({ name: nameSignal, count: () => getCount(), active: activeSignal })
|
|
|
412
638
|
| Consumer must know which props need getters | Consumer just passes values |
|
|
413
639
|
| Component must handle multiple input types | Component always gets DerivedSignal |
|
|
414
640
|
| Easy to forget `() =>` and get stale values | Props are always reactive |
|
|
415
|
-
| Inconsistent patterns across components | One pattern everywhere |
|
|
416
641
|
|
|
417
642
|
---
|
|
418
643
|
|
|
419
|
-
##
|
|
420
|
-
|
|
421
|
-
### linkedSignal (Angular's killer feature)
|
|
644
|
+
## Deep Reactivity
|
|
422
645
|
|
|
423
|
-
|
|
646
|
+
### proxy
|
|
424
647
|
|
|
425
|
-
|
|
426
|
-
linkedSignal<D>(fn: () => D): WritableSignal<D>
|
|
427
|
-
linkedSignal<S, D>(options: LinkedSignalOptions<S, D>): WritableSignal<D>
|
|
428
|
-
```
|
|
648
|
+
Create a deeply reactive proxy (used internally by `state()`).
|
|
429
649
|
|
|
430
650
|
```typescript
|
|
431
|
-
|
|
432
|
-
const options = signal(['a', 'b', 'c'])
|
|
433
|
-
const selected = linkedSignal(() => options.value[0])
|
|
651
|
+
const obj = proxy({ a: { b: { c: 1 } } })
|
|
434
652
|
|
|
435
|
-
console.log(
|
|
436
|
-
|
|
437
|
-
console.log(selected.value) // 'b'
|
|
653
|
+
effect(() => console.log('c changed:', obj.a.b.c))
|
|
654
|
+
effect(() => console.log('a changed:', obj.a))
|
|
438
655
|
|
|
439
|
-
|
|
440
|
-
flushSync()
|
|
441
|
-
console.log(selected.value) // 'x' (reset to computed value)
|
|
656
|
+
obj.a.b.c = 2 // Only triggers first effect (fine-grained!)
|
|
442
657
|
```
|
|
443
658
|
|
|
659
|
+
### toRaw
|
|
660
|
+
|
|
661
|
+
Get the original object from a proxy.
|
|
662
|
+
|
|
444
663
|
```typescript
|
|
445
|
-
|
|
446
|
-
const items = signal([1, 2, 3])
|
|
447
|
-
const selectedItem = linkedSignal({
|
|
448
|
-
source: () => items.value,
|
|
449
|
-
computation: (newItems, prev) => {
|
|
450
|
-
// Keep selection if still valid
|
|
451
|
-
if (prev && newItems.includes(prev.value)) {
|
|
452
|
-
return prev.value
|
|
453
|
-
}
|
|
454
|
-
return newItems[0]
|
|
455
|
-
}
|
|
456
|
-
})
|
|
664
|
+
const raw = toRaw(user) // Original non-reactive object
|
|
457
665
|
```
|
|
458
666
|
|
|
459
|
-
|
|
460
|
-
- Form inputs that reset when parent data changes
|
|
461
|
-
- Selection state that persists within valid options
|
|
462
|
-
- Derived values that can be temporarily overridden
|
|
463
|
-
|
|
464
|
-
### createSelector (Solid's O(n) to O(2) optimization)
|
|
667
|
+
### isReactive
|
|
465
668
|
|
|
466
|
-
|
|
669
|
+
Check if a value is a reactive proxy.
|
|
467
670
|
|
|
468
671
|
```typescript
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
): SelectorFn<T, U>
|
|
672
|
+
if (isReactive(value)) {
|
|
673
|
+
console.log('This is a proxy')
|
|
674
|
+
}
|
|
473
675
|
```
|
|
474
676
|
|
|
475
|
-
|
|
476
|
-
const selectedId = signal(1)
|
|
477
|
-
const isSelected = createSelector(() => selectedId.value)
|
|
677
|
+
---
|
|
478
678
|
|
|
479
|
-
|
|
480
|
-
items.forEach(item => {
|
|
481
|
-
effect(() => {
|
|
482
|
-
// Only runs when THIS item's selection state changes!
|
|
483
|
-
// When selectedId changes from 1 to 2:
|
|
484
|
-
// - Only item 1's effect runs (was selected, now not)
|
|
485
|
-
// - Only item 2's effect runs (was not selected, now is)
|
|
486
|
-
// - Other 998 items' effects DON'T run
|
|
487
|
-
if (isSelected(item.id)) {
|
|
488
|
-
highlight(item)
|
|
489
|
-
} else {
|
|
490
|
-
unhighlight(item)
|
|
491
|
-
}
|
|
492
|
-
})
|
|
493
|
-
})
|
|
494
|
-
```
|
|
679
|
+
## Utilities
|
|
495
680
|
|
|
496
|
-
###
|
|
681
|
+
### batch
|
|
497
682
|
|
|
498
|
-
|
|
683
|
+
Batch multiple signal updates into a single effect run. Essential for performance when doing multiple writes.
|
|
499
684
|
|
|
500
685
|
```typescript
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
interface EffectScope {
|
|
504
|
-
readonly active: boolean
|
|
505
|
-
readonly paused: boolean
|
|
506
|
-
run<R>(fn: () => R): R | undefined
|
|
507
|
-
stop(): void
|
|
508
|
-
pause(): void
|
|
509
|
-
resume(): void
|
|
510
|
-
}
|
|
511
|
-
```
|
|
686
|
+
const a = signal(1)
|
|
687
|
+
const b = signal(2)
|
|
512
688
|
|
|
513
|
-
|
|
514
|
-
const scope = effectScope()
|
|
689
|
+
effect.sync(() => console.log(a.value + b.value))
|
|
515
690
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
691
|
+
// Without batch: effect runs twice
|
|
692
|
+
a.value = 10 // Effect runs
|
|
693
|
+
b.value = 20 // Effect runs again
|
|
519
694
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
695
|
+
// With batch: effect runs once with final values
|
|
696
|
+
batch(() => {
|
|
697
|
+
a.value = 100
|
|
698
|
+
b.value = 200
|
|
523
699
|
})
|
|
700
|
+
// Effect runs once: 300
|
|
701
|
+
```
|
|
524
702
|
|
|
525
|
-
|
|
526
|
-
scope.pause()
|
|
527
|
-
count.value = 5 // Effect doesn't run
|
|
703
|
+
### untrack / peek
|
|
528
704
|
|
|
529
|
-
|
|
530
|
-
scope.resume()
|
|
705
|
+
Read signals without creating dependencies.
|
|
531
706
|
|
|
532
|
-
|
|
533
|
-
|
|
707
|
+
```typescript
|
|
708
|
+
effect(() => {
|
|
709
|
+
const a = count.value // Creates dependency
|
|
710
|
+
const b = untrack(() => other.value) // No dependency
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
// peek is an alias for untrack
|
|
714
|
+
const value = peek(() => signal.value)
|
|
534
715
|
```
|
|
535
716
|
|
|
536
|
-
###
|
|
717
|
+
### flushSync
|
|
537
718
|
|
|
538
|
-
|
|
719
|
+
Synchronously flush all pending effects. Useful for testing and ensuring effects have run.
|
|
539
720
|
|
|
540
721
|
```typescript
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
722
|
+
count.value = 5
|
|
723
|
+
flushSync() // Effects run NOW, not on next microtask
|
|
724
|
+
|
|
725
|
+
// Can also wrap a function
|
|
726
|
+
flushSync(() => {
|
|
727
|
+
count.value = 10
|
|
728
|
+
// Effects for this change run before flushSync returns
|
|
544
729
|
})
|
|
545
730
|
```
|
|
546
731
|
|
|
547
|
-
###
|
|
732
|
+
### tick
|
|
548
733
|
|
|
549
|
-
|
|
734
|
+
Wait for the next update cycle (async). Returns a promise that resolves after all pending effects have run.
|
|
550
735
|
|
|
551
736
|
```typescript
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
// Inside a scope
|
|
555
|
-
}
|
|
737
|
+
count.value = 5
|
|
738
|
+
await tick() // Effects have run
|
|
556
739
|
```
|
|
557
740
|
|
|
558
741
|
---
|
|
@@ -597,7 +780,7 @@ A Date with reactive getters/setters.
|
|
|
597
780
|
const date = new ReactiveDate()
|
|
598
781
|
|
|
599
782
|
effect(() => {
|
|
600
|
-
console.log(date.getHours()) // Re-runs when
|
|
783
|
+
console.log(date.getHours()) // Re-runs when hours change
|
|
601
784
|
})
|
|
602
785
|
|
|
603
786
|
date.setHours(12) // Triggers effect
|
|
@@ -605,128 +788,38 @@ date.setHours(12) // Triggers effect
|
|
|
605
788
|
|
|
606
789
|
---
|
|
607
790
|
|
|
608
|
-
## Utilities
|
|
609
|
-
|
|
610
|
-
### batch
|
|
611
|
-
|
|
612
|
-
Batch multiple signal updates into a single effect run.
|
|
613
|
-
|
|
614
|
-
```typescript
|
|
615
|
-
const a = signal(1)
|
|
616
|
-
const b = signal(2)
|
|
617
|
-
|
|
618
|
-
effect(() => console.log(a.value + b.value))
|
|
619
|
-
|
|
620
|
-
batch(() => {
|
|
621
|
-
a.value = 10
|
|
622
|
-
b.value = 20
|
|
623
|
-
})
|
|
624
|
-
// Effect runs once with final values, not twice
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
### untrack / peek
|
|
628
|
-
|
|
629
|
-
Read signals without creating dependencies.
|
|
630
|
-
|
|
631
|
-
```typescript
|
|
632
|
-
effect(() => {
|
|
633
|
-
const a = count.value // Creates dependency
|
|
634
|
-
const b = untrack(() => other.value) // No dependency
|
|
635
|
-
})
|
|
636
|
-
|
|
637
|
-
// peek is an alias for untrack
|
|
638
|
-
const value = peek(() => signal.value)
|
|
639
|
-
```
|
|
640
|
-
|
|
641
|
-
### flushSync
|
|
642
|
-
|
|
643
|
-
Synchronously flush all pending effects.
|
|
644
|
-
|
|
645
|
-
```typescript
|
|
646
|
-
count.value = 5
|
|
647
|
-
flushSync() // Effects run NOW, not on next microtask
|
|
648
|
-
```
|
|
649
|
-
|
|
650
|
-
### tick
|
|
651
|
-
|
|
652
|
-
Wait for the next update cycle (async).
|
|
653
|
-
|
|
654
|
-
```typescript
|
|
655
|
-
count.value = 5
|
|
656
|
-
await tick() // Effects have run
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
---
|
|
660
|
-
|
|
661
|
-
## Deep Reactivity
|
|
662
|
-
|
|
663
|
-
### proxy
|
|
664
|
-
|
|
665
|
-
Create a deeply reactive proxy (used internally by `state()`).
|
|
666
|
-
|
|
667
|
-
```typescript
|
|
668
|
-
const obj = proxy({ a: { b: { c: 1 } } })
|
|
669
|
-
|
|
670
|
-
effect(() => console.log('c changed:', obj.a.b.c))
|
|
671
|
-
effect(() => console.log('a changed:', obj.a))
|
|
672
|
-
|
|
673
|
-
obj.a.b.c = 2 // Only triggers first effect (fine-grained!)
|
|
674
|
-
```
|
|
675
|
-
|
|
676
|
-
### toRaw
|
|
677
|
-
|
|
678
|
-
Get the original object from a proxy.
|
|
679
|
-
|
|
680
|
-
```typescript
|
|
681
|
-
const raw = toRaw(user) // Original non-reactive object
|
|
682
|
-
```
|
|
683
|
-
|
|
684
|
-
### isReactive
|
|
685
|
-
|
|
686
|
-
Check if a value is a reactive proxy.
|
|
687
|
-
|
|
688
|
-
```typescript
|
|
689
|
-
if (isReactive(value)) {
|
|
690
|
-
console.log('This is a proxy')
|
|
691
|
-
}
|
|
692
|
-
```
|
|
693
|
-
|
|
694
|
-
---
|
|
695
|
-
|
|
696
791
|
## Equality Functions
|
|
697
792
|
|
|
698
793
|
Control when signals trigger updates:
|
|
699
794
|
|
|
700
795
|
```typescript
|
|
701
|
-
import {
|
|
796
|
+
import {
|
|
797
|
+
signal, derived,
|
|
798
|
+
equals, // Object.is (default for signals)
|
|
799
|
+
deepEquals, // Bun.deepEquals (default for derived)
|
|
800
|
+
safeEquals, // Handles NaN correctly
|
|
801
|
+
shallowEquals, // One level deep comparison
|
|
802
|
+
neverEquals, // Always trigger
|
|
803
|
+
alwaysEquals, // Never trigger
|
|
804
|
+
createEquals // Custom equality
|
|
805
|
+
} from '@rlabs-inc/signals'
|
|
702
806
|
|
|
703
|
-
// signal()
|
|
807
|
+
// signal() uses Object.is (reference equality)
|
|
704
808
|
const a = signal(0)
|
|
705
809
|
|
|
706
|
-
// derived()
|
|
707
|
-
//
|
|
810
|
+
// derived() uses Bun.deepEquals (structural equality)
|
|
811
|
+
// Prevents unnecessary propagation when computed values are structurally identical
|
|
708
812
|
const items = signal([1, 2, 3])
|
|
709
813
|
const doubled = derived(() => items.value.map(x => x * 2))
|
|
710
|
-
// If doubled produces [2, 4, 6] again, downstream effects won't re-run
|
|
711
814
|
|
|
712
|
-
// Deep equality
|
|
815
|
+
// Deep equality for signals
|
|
713
816
|
const c = signal({ a: 1 }, { equals: deepEquals })
|
|
714
817
|
c.value = { a: 1 } // Won't trigger - deeply equal
|
|
715
818
|
|
|
716
|
-
// Safe equality - handles NaN correctly
|
|
717
|
-
const d = signal(NaN, { equals: safeEquals })
|
|
718
|
-
|
|
719
|
-
// Shallow comparison - compares one level deep
|
|
720
|
-
const e = signal({ a: 1 }, { equals: shallowEquals })
|
|
721
|
-
|
|
722
819
|
// Always trigger updates
|
|
723
820
|
const f = signal(0, { equals: neverEquals })
|
|
724
821
|
f.value = 0 // Still triggers!
|
|
725
822
|
|
|
726
|
-
// Never trigger updates
|
|
727
|
-
const g = signal(0, { equals: alwaysEquals })
|
|
728
|
-
g.value = 100 // Doesn't trigger
|
|
729
|
-
|
|
730
823
|
// Custom equality
|
|
731
824
|
const customEquals = createEquals((a, b) =>
|
|
732
825
|
JSON.stringify(a) === JSON.stringify(b)
|
|
@@ -735,11 +828,12 @@ const h = signal([], { equals: customEquals })
|
|
|
735
828
|
```
|
|
736
829
|
|
|
737
830
|
**Default equality by primitive:**
|
|
831
|
+
|
|
738
832
|
| Primitive | Default | Reason |
|
|
739
833
|
|-----------|---------|--------|
|
|
740
834
|
| `signal()` | `Object.is` | User-controlled input - reference equality |
|
|
741
|
-
| `derived()` | `
|
|
742
|
-
| `linkedSignal()` | `
|
|
835
|
+
| `derived()` | `deepEquals` | Computed output - structural equality prevents unnecessary work |
|
|
836
|
+
| `linkedSignal()` | `deepEquals` | Computed output - structural equality |
|
|
743
837
|
|
|
744
838
|
---
|
|
745
839
|
|
|
@@ -774,7 +868,7 @@ effect(() => {
|
|
|
774
868
|
count.value = count.value + 1 // Always triggers itself
|
|
775
869
|
})
|
|
776
870
|
|
|
777
|
-
// GOOD - add a guard
|
|
871
|
+
// GOOD - add a guard or use untrack
|
|
778
872
|
effect(() => {
|
|
779
873
|
if (count.value < 100) {
|
|
780
874
|
count.value++
|
|
@@ -784,30 +878,6 @@ effect(() => {
|
|
|
784
878
|
|
|
785
879
|
---
|
|
786
880
|
|
|
787
|
-
## Performance
|
|
788
|
-
|
|
789
|
-
This library is designed for performance:
|
|
790
|
-
|
|
791
|
-
| Operation | Complexity | Notes |
|
|
792
|
-
|-----------|------------|-------|
|
|
793
|
-
| Signal read/write | O(1) | Direct property access |
|
|
794
|
-
| Derived read | O(1) | Cached after first computation |
|
|
795
|
-
| Effect trigger | O(deps) | Only runs if dependencies change |
|
|
796
|
-
| `batch()` | O(1) cycle | Multiple updates, single flush |
|
|
797
|
-
| `createSelector()` | O(2) | Only changed items' effects run |
|
|
798
|
-
| Proxy property access | O(1) | Per-property signal lookup |
|
|
799
|
-
| `ReactiveMap.get()` | O(1) | Per-key tracking |
|
|
800
|
-
|
|
801
|
-
**Key optimizations:**
|
|
802
|
-
- **Lazy evaluation** - Deriveds only compute when read
|
|
803
|
-
- **Version-based deduplication** - No duplicate dependency tracking
|
|
804
|
-
- **Linked list effect tree** - O(1) effect insertion/removal
|
|
805
|
-
- **Microtask batching** - Updates coalesce automatically
|
|
806
|
-
- **Per-property signals** - Fine-grained updates at any depth
|
|
807
|
-
- **FinalizationRegistry cleanup** - Automatic memory management
|
|
808
|
-
|
|
809
|
-
---
|
|
810
|
-
|
|
811
881
|
## Framework Comparison
|
|
812
882
|
|
|
813
883
|
| Feature | @rlabs-inc/signals | Svelte 5 | Vue 3 | Angular | Solid.js |
|
|
@@ -815,11 +885,13 @@ This library is designed for performance:
|
|
|
815
885
|
| `signal()` | `signal()` | `$state` | `ref()` | `signal()` | `createSignal()` |
|
|
816
886
|
| `derived()` | `derived()` | `$derived` | `computed()` | `computed()` | `createMemo()` |
|
|
817
887
|
| `effect()` | `effect()` | `$effect` | `watchEffect()` | `effect()` | `createEffect()` |
|
|
888
|
+
| `effect.sync()` | `effect.sync()` | `$effect.pre` | - | - | - |
|
|
818
889
|
| Deep reactivity | `state()` | `$state` | `reactive()` | - | - |
|
|
819
890
|
| `linkedSignal()` | Yes | - | - | Yes | - |
|
|
820
891
|
| `createSelector()` | Yes | - | - | - | Yes |
|
|
821
892
|
| `effectScope()` | Yes | - | Yes | - | - |
|
|
822
893
|
| `slot()` / `slotArray()` | Yes | - | - | - | - |
|
|
894
|
+
| `reactiveProps()` | Yes | - | - | - | - |
|
|
823
895
|
| Compiler required | No | Yes | No | No | No |
|
|
824
896
|
| DOM integration | No | Yes | Yes | Yes | Yes |
|
|
825
897
|
|
|
@@ -851,6 +923,49 @@ set(src, 10)
|
|
|
851
923
|
|
|
852
924
|
---
|
|
853
925
|
|
|
926
|
+
## Type Exports
|
|
927
|
+
|
|
928
|
+
```typescript
|
|
929
|
+
import type {
|
|
930
|
+
// Core types
|
|
931
|
+
Signal,
|
|
932
|
+
Source,
|
|
933
|
+
Reaction,
|
|
934
|
+
Derived,
|
|
935
|
+
Effect,
|
|
936
|
+
Value,
|
|
937
|
+
|
|
938
|
+
// Public API types
|
|
939
|
+
ReadableSignal,
|
|
940
|
+
WritableSignal,
|
|
941
|
+
DerivedSignal,
|
|
942
|
+
DisposeFn,
|
|
943
|
+
CleanupFn,
|
|
944
|
+
EffectFn,
|
|
945
|
+
|
|
946
|
+
// Binding types
|
|
947
|
+
Binding,
|
|
948
|
+
ReadonlyBinding,
|
|
949
|
+
|
|
950
|
+
// Slot types
|
|
951
|
+
Slot,
|
|
952
|
+
SlotArray,
|
|
953
|
+
|
|
954
|
+
// Props types
|
|
955
|
+
PropInput,
|
|
956
|
+
PropsInput,
|
|
957
|
+
ReactiveProps,
|
|
958
|
+
|
|
959
|
+
// Advanced types
|
|
960
|
+
LinkedSignalOptions,
|
|
961
|
+
SelectorFn,
|
|
962
|
+
EffectScope,
|
|
963
|
+
Equals,
|
|
964
|
+
} from '@rlabs-inc/signals'
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
---
|
|
968
|
+
|
|
854
969
|
## License
|
|
855
970
|
|
|
856
971
|
MIT
|