@ersbeth/picoflow 0.2.4 → 1.0.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/.cursor/plans/update-js-e795d61b.plan.md +567 -0
- package/.gitlab-ci.yml +24 -0
- package/.vscode/settings.json +3 -3
- package/CHANGELOG.md +51 -0
- package/IMPLEMENTATION_GUIDE.md +1578 -0
- package/README.md +9 -134
- package/biome.json +32 -32
- package/dist/picoflow.js +610 -436
- package/dist/types/advanced/array.d.ts +0 -6
- package/dist/types/advanced/array.d.ts.map +1 -1
- package/dist/types/advanced/index.d.ts +5 -5
- package/dist/types/advanced/index.d.ts.map +1 -1
- package/dist/types/advanced/map.d.ts +114 -23
- package/dist/types/advanced/map.d.ts.map +1 -1
- package/dist/types/advanced/resource.d.ts +51 -12
- package/dist/types/advanced/resource.d.ts.map +1 -1
- package/dist/types/advanced/resourceAsync.d.ts +28 -13
- package/dist/types/advanced/resourceAsync.d.ts.map +1 -1
- package/dist/types/advanced/stream.d.ts +74 -16
- package/dist/types/advanced/stream.d.ts.map +1 -1
- package/dist/types/advanced/streamAsync.d.ts +69 -15
- package/dist/types/advanced/streamAsync.d.ts.map +1 -1
- package/dist/types/basic/constant.d.ts +44 -16
- package/dist/types/basic/constant.d.ts.map +1 -1
- package/dist/types/basic/derivation.d.ts +73 -24
- package/dist/types/basic/derivation.d.ts.map +1 -1
- package/dist/types/basic/disposable.d.ts +65 -6
- package/dist/types/basic/disposable.d.ts.map +1 -1
- package/dist/types/basic/effect.d.ts +27 -16
- package/dist/types/basic/effect.d.ts.map +1 -1
- package/dist/types/basic/index.d.ts +7 -8
- package/dist/types/basic/index.d.ts.map +1 -1
- package/dist/types/basic/observable.d.ts +62 -13
- package/dist/types/basic/observable.d.ts.map +1 -1
- package/dist/types/basic/signal.d.ts +35 -6
- package/dist/types/basic/signal.d.ts.map +1 -1
- package/dist/types/basic/state.d.ts +25 -4
- package/dist/types/basic/state.d.ts.map +1 -1
- package/dist/types/basic/trackingContext.d.ts +33 -0
- package/dist/types/basic/trackingContext.d.ts.map +1 -0
- package/dist/types/creators.d.ts +271 -26
- package/dist/types/creators.d.ts.map +1 -1
- package/dist/types/index.d.ts +60 -7
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/solid/converters.d.ts +5 -5
- package/dist/types/solid/converters.d.ts.map +1 -1
- package/dist/types/solid/index.d.ts +2 -2
- package/dist/types/solid/index.d.ts.map +1 -1
- package/dist/types/solid/primitives.d.ts +96 -4
- package/dist/types/solid/primitives.d.ts.map +1 -1
- package/docs/.vitepress/config.mts +110 -0
- package/docs/api/classes/FlowArray.md +489 -0
- package/docs/api/classes/FlowConstant.md +350 -0
- package/docs/api/classes/FlowDerivation.md +334 -0
- package/docs/api/classes/FlowEffect.md +100 -0
- package/docs/api/classes/FlowMap.md +512 -0
- package/docs/api/classes/FlowObservable.md +306 -0
- package/docs/api/classes/FlowResource.md +380 -0
- package/docs/api/classes/FlowResourceAsync.md +362 -0
- package/docs/api/classes/FlowSignal.md +160 -0
- package/docs/api/classes/FlowState.md +368 -0
- package/docs/api/classes/FlowStream.md +367 -0
- package/docs/api/classes/FlowStreamAsync.md +364 -0
- package/docs/api/classes/SolidDerivation.md +75 -0
- package/docs/api/classes/SolidResource.md +91 -0
- package/docs/api/classes/SolidState.md +71 -0
- package/docs/api/classes/TrackingContext.md +33 -0
- package/docs/api/functions/array.md +58 -0
- package/docs/api/functions/constant.md +45 -0
- package/docs/api/functions/derivation.md +53 -0
- package/docs/api/functions/effect.md +49 -0
- package/docs/api/functions/from.md +220 -0
- package/docs/api/functions/isDisposable.md +49 -0
- package/docs/api/functions/map.md +57 -0
- package/docs/api/functions/resource.md +52 -0
- package/docs/api/functions/resourceAsync.md +50 -0
- package/docs/api/functions/signal.md +36 -0
- package/docs/api/functions/state.md +47 -0
- package/docs/api/functions/stream.md +53 -0
- package/docs/api/functions/streamAsync.md +50 -0
- package/docs/api/index.md +118 -0
- package/docs/api/interfaces/FlowDisposable.md +65 -0
- package/docs/api/interfaces/SolidObservable.md +19 -0
- package/docs/api/type-aliases/FlowArrayAction.md +49 -0
- package/docs/api/type-aliases/FlowStreamDisposer.md +15 -0
- package/docs/api/type-aliases/FlowStreamSetter.md +27 -0
- package/docs/api/type-aliases/FlowStreamUpdater.md +32 -0
- package/docs/api/type-aliases/NotPromise.md +18 -0
- package/docs/api/type-aliases/SolidGetter.md +17 -0
- package/docs/api/typedoc-sidebar.json +1 -0
- package/docs/examples/examples.md +2313 -0
- package/docs/examples/patterns.md +649 -0
- package/docs/guide/advanced/disposal.md +426 -0
- package/docs/guide/advanced/solidjs.md +221 -0
- package/docs/guide/advanced/upgrading.md +464 -0
- package/docs/guide/introduction/concepts.md +56 -0
- package/docs/guide/introduction/conventions.md +61 -0
- package/docs/guide/introduction/getting-started.md +134 -0
- package/docs/guide/introduction/lifecycle.md +371 -0
- package/docs/guide/primitives/array.md +400 -0
- package/docs/guide/primitives/constant.md +380 -0
- package/docs/guide/primitives/derivations.md +348 -0
- package/docs/guide/primitives/effects.md +458 -0
- package/docs/guide/primitives/map.md +387 -0
- package/docs/guide/primitives/overview.md +175 -0
- package/docs/guide/primitives/resources.md +858 -0
- package/docs/guide/primitives/signal.md +259 -0
- package/docs/guide/primitives/state.md +368 -0
- package/docs/guide/primitives/streams.md +931 -0
- package/docs/index.md +47 -0
- package/docs/public/logo.svg +1 -0
- package/package.json +57 -41
- package/src/advanced/array.ts +208 -210
- package/src/advanced/index.ts +7 -7
- package/src/advanced/map.ts +178 -68
- package/src/advanced/resource.ts +87 -43
- package/src/advanced/resourceAsync.ts +62 -42
- package/src/advanced/stream.ts +113 -50
- package/src/advanced/streamAsync.ts +120 -61
- package/src/basic/constant.ts +82 -49
- package/src/basic/derivation.ts +128 -84
- package/src/basic/disposable.ts +74 -15
- package/src/basic/effect.ts +85 -77
- package/src/basic/index.ts +7 -8
- package/src/basic/observable.ts +94 -36
- package/src/basic/signal.ts +133 -105
- package/src/basic/state.ts +46 -25
- package/src/basic/trackingContext.ts +45 -0
- package/src/creators.ts +297 -54
- package/src/index.ts +96 -43
- package/src/solid/converters.ts +186 -67
- package/src/solid/index.ts +8 -2
- package/src/solid/primitives.ts +167 -65
- package/test/array.test.ts +592 -612
- package/test/constant.test.ts +31 -33
- package/test/derivation.test.ts +531 -536
- package/test/effect.test.ts +21 -21
- package/test/map.test.ts +233 -137
- package/test/resource.test.ts +119 -121
- package/test/resourceAsync.test.ts +98 -100
- package/test/signal.test.ts +51 -55
- package/test/state.test.ts +186 -168
- package/test/stream.test.ts +189 -189
- package/test/streamAsync.test.ts +186 -186
- package/tsconfig.json +19 -18
- package/typedoc.json +37 -0
- package/vite.config.ts +23 -23
- package/vitest.config.ts +7 -7
- package/api/doc/index.md +0 -31
- package/api/doc/picoflow.array.md +0 -55
- package/api/doc/picoflow.constant.md +0 -55
- package/api/doc/picoflow.derivation.md +0 -55
- package/api/doc/picoflow.effect.md +0 -55
- package/api/doc/picoflow.flowarray._constructor_.md +0 -49
- package/api/doc/picoflow.flowarray._lastaction.md +0 -13
- package/api/doc/picoflow.flowarray.clear.md +0 -17
- package/api/doc/picoflow.flowarray.dispose.md +0 -55
- package/api/doc/picoflow.flowarray.get.md +0 -19
- package/api/doc/picoflow.flowarray.length.md +0 -13
- package/api/doc/picoflow.flowarray.md +0 -273
- package/api/doc/picoflow.flowarray.pop.md +0 -17
- package/api/doc/picoflow.flowarray.push.md +0 -53
- package/api/doc/picoflow.flowarray.set.md +0 -53
- package/api/doc/picoflow.flowarray.setitem.md +0 -69
- package/api/doc/picoflow.flowarray.shift.md +0 -17
- package/api/doc/picoflow.flowarray.splice.md +0 -85
- package/api/doc/picoflow.flowarray.unshift.md +0 -53
- package/api/doc/picoflow.flowarrayaction.md +0 -37
- package/api/doc/picoflow.flowconstant._constructor_.md +0 -49
- package/api/doc/picoflow.flowconstant.get.md +0 -25
- package/api/doc/picoflow.flowconstant.md +0 -88
- package/api/doc/picoflow.flowderivation._constructor_.md +0 -49
- package/api/doc/picoflow.flowderivation.get.md +0 -23
- package/api/doc/picoflow.flowderivation.md +0 -86
- package/api/doc/picoflow.flowdisposable.dispose.md +0 -55
- package/api/doc/picoflow.flowdisposable.md +0 -43
- package/api/doc/picoflow.floweffect._constructor_.md +0 -54
- package/api/doc/picoflow.floweffect.dispose.md +0 -21
- package/api/doc/picoflow.floweffect.disposed.md +0 -13
- package/api/doc/picoflow.floweffect.md +0 -131
- package/api/doc/picoflow.flowgetter.md +0 -15
- package/api/doc/picoflow.flowmap._lastdeleted.md +0 -21
- package/api/doc/picoflow.flowmap._lastset.md +0 -21
- package/api/doc/picoflow.flowmap.delete.md +0 -61
- package/api/doc/picoflow.flowmap.md +0 -133
- package/api/doc/picoflow.flowmap.setat.md +0 -77
- package/api/doc/picoflow.flowobservable.get.md +0 -19
- package/api/doc/picoflow.flowobservable.md +0 -68
- package/api/doc/picoflow.flowobservable.subscribe.md +0 -55
- package/api/doc/picoflow.flowresource._constructor_.md +0 -49
- package/api/doc/picoflow.flowresource.fetch.md +0 -27
- package/api/doc/picoflow.flowresource.get.md +0 -23
- package/api/doc/picoflow.flowresource.md +0 -100
- package/api/doc/picoflow.flowresourceasync._constructor_.md +0 -49
- package/api/doc/picoflow.flowresourceasync.fetch.md +0 -27
- package/api/doc/picoflow.flowresourceasync.get.md +0 -23
- package/api/doc/picoflow.flowresourceasync.md +0 -100
- package/api/doc/picoflow.flowsignal.dispose.md +0 -59
- package/api/doc/picoflow.flowsignal.disposed.md +0 -18
- package/api/doc/picoflow.flowsignal.md +0 -112
- package/api/doc/picoflow.flowsignal.trigger.md +0 -21
- package/api/doc/picoflow.flowstate.md +0 -52
- package/api/doc/picoflow.flowstate.set.md +0 -61
- package/api/doc/picoflow.flowstream._constructor_.md +0 -49
- package/api/doc/picoflow.flowstream.dispose.md +0 -21
- package/api/doc/picoflow.flowstream.get.md +0 -23
- package/api/doc/picoflow.flowstream.md +0 -100
- package/api/doc/picoflow.flowstreamasync._constructor_.md +0 -54
- package/api/doc/picoflow.flowstreamasync.dispose.md +0 -21
- package/api/doc/picoflow.flowstreamasync.get.md +0 -23
- package/api/doc/picoflow.flowstreamasync.md +0 -100
- package/api/doc/picoflow.flowstreamdisposer.md +0 -13
- package/api/doc/picoflow.flowstreamsetter.md +0 -13
- package/api/doc/picoflow.flowstreamupdater.md +0 -19
- package/api/doc/picoflow.flowwatcher.md +0 -15
- package/api/doc/picoflow.from.md +0 -55
- package/api/doc/picoflow.from_1.md +0 -55
- package/api/doc/picoflow.from_2.md +0 -55
- package/api/doc/picoflow.from_3.md +0 -55
- package/api/doc/picoflow.from_4.md +0 -55
- package/api/doc/picoflow.from_5.md +0 -55
- package/api/doc/picoflow.isdisposable.md +0 -55
- package/api/doc/picoflow.map.md +0 -59
- package/api/doc/picoflow.md +0 -544
- package/api/doc/picoflow.resource.md +0 -55
- package/api/doc/picoflow.resourceasync.md +0 -55
- package/api/doc/picoflow.signal.md +0 -19
- package/api/doc/picoflow.solidderivation._constructor_.md +0 -49
- package/api/doc/picoflow.solidderivation.get.md +0 -13
- package/api/doc/picoflow.solidderivation.md +0 -94
- package/api/doc/picoflow.solidgetter.md +0 -13
- package/api/doc/picoflow.solidobservable.get.md +0 -13
- package/api/doc/picoflow.solidobservable.md +0 -57
- package/api/doc/picoflow.solidresource._constructor_.md +0 -49
- package/api/doc/picoflow.solidresource.get.md +0 -13
- package/api/doc/picoflow.solidresource.latest.md +0 -13
- package/api/doc/picoflow.solidresource.md +0 -157
- package/api/doc/picoflow.solidresource.refetch.md +0 -13
- package/api/doc/picoflow.solidresource.state.md +0 -13
- package/api/doc/picoflow.solidstate._constructor_.md +0 -49
- package/api/doc/picoflow.solidstate.get.md +0 -13
- package/api/doc/picoflow.solidstate.md +0 -115
- package/api/doc/picoflow.solidstate.set.md +0 -13
- package/api/doc/picoflow.state.md +0 -55
- package/api/doc/picoflow.stream.md +0 -55
- package/api/doc/picoflow.streamasync.md +0 -55
- package/api/picoflow.public.api.md +0 -244
- package/api-extractor.json +0 -61
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Signals
|
|
2
|
+
|
|
3
|
+
A **signal** is a reactive primitive that represents an event or notification. Unlike state or derivations, signals don't hold values - they simply notify watchers that something has happened.
|
|
4
|
+
|
|
5
|
+
Imagine a refresh button on a web page. When clicked, it doesn't need to pass data - it just needs to tell the system "hey, refresh now!" That's exactly what signals do.
|
|
6
|
+
|
|
7
|
+
### Key Characteristics
|
|
8
|
+
|
|
9
|
+
- **No value**: Signals don't store or return data
|
|
10
|
+
- **Pure notification**: They exist solely to trigger reactions
|
|
11
|
+
- **Lightweight**: Perfect for events that don't need to carry information
|
|
12
|
+
- **Explicit tracking**: Use `.watch(t)` to track them in effects
|
|
13
|
+
|
|
14
|
+
## When to Use Signals
|
|
15
|
+
|
|
16
|
+
Use signals when you need to:
|
|
17
|
+
|
|
18
|
+
- ✅ Trigger manual refreshes or updates
|
|
19
|
+
- ✅ Coordinate actions between independent parts of your app
|
|
20
|
+
- ✅ Signal that an event has occurred without sending data
|
|
21
|
+
- ✅ Create synchronization points in reactive flows
|
|
22
|
+
|
|
23
|
+
Don't use signals when:
|
|
24
|
+
|
|
25
|
+
- ❌ You need to pass data (use state instead)
|
|
26
|
+
- ❌ You're tracking continuous streams of events (use streams instead)
|
|
27
|
+
- ❌ The value itself matters (use state or derivation)
|
|
28
|
+
|
|
29
|
+
## Creating Signals
|
|
30
|
+
|
|
31
|
+
Creating a signal is straightforward:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { signal } from '@ersbeth/picoflow'
|
|
35
|
+
|
|
36
|
+
const $refreshTrigger = signal()
|
|
37
|
+
const $saveComplete = signal()
|
|
38
|
+
const $userLoggedOut = signal()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Signals don't take any parameters since they don't hold values.
|
|
42
|
+
|
|
43
|
+
## Using Signals
|
|
44
|
+
|
|
45
|
+
Signals have three main operations: watching, triggering, and disposing.
|
|
46
|
+
|
|
47
|
+
### Watching Signals with `.watch(t)`
|
|
48
|
+
|
|
49
|
+
Inside an effect or derivation, use `.watch(t)` to track a signal:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { signal, effect } from '@ersbeth/picoflow'
|
|
53
|
+
|
|
54
|
+
const $refresh = signal()
|
|
55
|
+
|
|
56
|
+
effect((t) => {
|
|
57
|
+
$refresh.watch(t)
|
|
58
|
+
console.log('Refresh triggered!')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
$refresh.trigger() // Logs: "Refresh triggered!"
|
|
62
|
+
$refresh.trigger() // Logs: "Refresh triggered!" again
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Every time the signal is triggered, the effect re-executes.
|
|
66
|
+
|
|
67
|
+
### Triggering Signals with `.trigger()`
|
|
68
|
+
|
|
69
|
+
Call `.trigger()` to notify all watchers:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
const $notification = signal()
|
|
73
|
+
|
|
74
|
+
// Setup an effect that watches the signal
|
|
75
|
+
effect((t) => {
|
|
76
|
+
$notification.watch(t)
|
|
77
|
+
showNotification('New activity!')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// Trigger from anywhere
|
|
81
|
+
function onNewMessage() {
|
|
82
|
+
$notification.trigger()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function onNewLike() {
|
|
86
|
+
$notification.trigger()
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Disposing Signals with `.dispose()`
|
|
91
|
+
|
|
92
|
+
Signals can be disposed to free resources and prevent memory leaks:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
const $mySignal = signal()
|
|
96
|
+
|
|
97
|
+
// Use the signal
|
|
98
|
+
effect((t) => {
|
|
99
|
+
$mySignal.watch(t)
|
|
100
|
+
console.log('Signal triggered!')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// Later, clean up
|
|
104
|
+
$mySignal.dispose()
|
|
105
|
+
|
|
106
|
+
// Subsequent operations will throw
|
|
107
|
+
$mySignal.trigger() // Error: Primitive is disposed
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Lifecyle
|
|
111
|
+
|
|
112
|
+
When you create an effect that watches a signal:
|
|
113
|
+
|
|
114
|
+
1. **Registration**: The signal registers the effect as a watcher
|
|
115
|
+
2. **Waiting**: The signal waits for `.trigger()` to be called
|
|
116
|
+
3. **Notification**: When triggered, all watching effects are scheduled to run
|
|
117
|
+
4. **Re-execution**: Each watching effect re-executes its function
|
|
118
|
+
|
|
119
|
+
```mermaid
|
|
120
|
+
sequenceDiagram
|
|
121
|
+
participant User
|
|
122
|
+
participant Signal as $refresh (Signal)
|
|
123
|
+
participant Effect
|
|
124
|
+
|
|
125
|
+
Note over User,Effect: 1. Setup Phase
|
|
126
|
+
User->>Effect: Create effect
|
|
127
|
+
activate Effect
|
|
128
|
+
Effect->>Signal: watch(t)
|
|
129
|
+
Note over Signal: Register Effect as watcher
|
|
130
|
+
Signal-->>Effect: Registered
|
|
131
|
+
Effect->>Effect: Execute function
|
|
132
|
+
Note over Effect: Log: "Ready to refresh"
|
|
133
|
+
deactivate Effect
|
|
134
|
+
|
|
135
|
+
Note over User,Effect: 2. Trigger Phase
|
|
136
|
+
User->>Signal: trigger()
|
|
137
|
+
activate Signal
|
|
138
|
+
Note over Signal: Notify all watchers
|
|
139
|
+
Signal->>Effect: Schedule execution
|
|
140
|
+
deactivate Signal
|
|
141
|
+
|
|
142
|
+
activate Effect
|
|
143
|
+
Note over Effect: Re-execute function
|
|
144
|
+
Effect->>Effect: Perform refresh
|
|
145
|
+
Note over Effect: Log: "Refreshing..."
|
|
146
|
+
deactivate Effect
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Best Practices
|
|
150
|
+
|
|
151
|
+
### Name Signals Clearly
|
|
152
|
+
|
|
153
|
+
Use names that describe the event, not the action:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// ✅ Good - describes the event
|
|
157
|
+
const $refreshRequested = signal()
|
|
158
|
+
const $saveComplete = signal()
|
|
159
|
+
const $validationFailed = signal()
|
|
160
|
+
|
|
161
|
+
// ❌ Bad - describes the handler
|
|
162
|
+
const $doRefresh = signal()
|
|
163
|
+
const $handleSave = signal()
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Common Pitfalls
|
|
167
|
+
|
|
168
|
+
### Using Signals When State Would Be Better
|
|
169
|
+
|
|
170
|
+
**Problem**: Using a signal when you actually need to track a value.
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
// ❌ Bad - should use state
|
|
174
|
+
const $userUpdated = signal()
|
|
175
|
+
let currentUser: User | null = null
|
|
176
|
+
|
|
177
|
+
effect((t) => {
|
|
178
|
+
$userUpdated.watch(t)
|
|
179
|
+
if (currentUser) {
|
|
180
|
+
renderUser(currentUser)
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
function setUser(user: User) {
|
|
185
|
+
currentUser = user
|
|
186
|
+
$userUpdated.trigger()
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Solution**: Use state to hold the value:
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
// ✅ Good - state holds the value
|
|
194
|
+
const $currentUser = state<User | null>(null)
|
|
195
|
+
|
|
196
|
+
effect((t) => {
|
|
197
|
+
const user = $currentUser.get(t)
|
|
198
|
+
if (user) {
|
|
199
|
+
renderUser(user)
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
function setUser(user: User) {
|
|
204
|
+
$currentUser.set(user)
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Forgetting to Watch the Signal
|
|
209
|
+
|
|
210
|
+
**Problem**: Calling `.trigger()` without any watchers.
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// ❌ Bad - nothing watches the signal
|
|
214
|
+
const $refresh = signal()
|
|
215
|
+
|
|
216
|
+
effect((t) => {
|
|
217
|
+
// Forgot to watch!
|
|
218
|
+
performRefresh()
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
$refresh.trigger() // Effect won't run
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Solution**: Make sure to call `.watch(t)`:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// ✅ Good - explicitly watch the signal
|
|
228
|
+
const $refresh = signal()
|
|
229
|
+
|
|
230
|
+
effect((t) => {
|
|
231
|
+
$refresh.watch(t) // Now it will react!
|
|
232
|
+
performRefresh()
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
$refresh.trigger() // Effect runs
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Over-Triggering
|
|
239
|
+
|
|
240
|
+
**Problem**: Triggering a signal in a loop or too frequently.
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
// ❌ Bad - triggers in a loop
|
|
244
|
+
for (let i = 0; i < 100; i++) {
|
|
245
|
+
processItem(i)
|
|
246
|
+
$itemProcessed.trigger() // 100 triggers!
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Solution**: Trigger once after the batch:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
// ✅ Good - single trigger after batch
|
|
254
|
+
for (let i = 0; i < 100; i++) {
|
|
255
|
+
processItem(i)
|
|
256
|
+
}
|
|
257
|
+
$batchProcessed.trigger() // 1 trigger
|
|
258
|
+
```
|
|
259
|
+
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# State
|
|
2
|
+
|
|
3
|
+
**State** is a reactive primitive that holds a mutable value. When you update the state, all effects and derivations watching it automatically re-execute.
|
|
4
|
+
|
|
5
|
+
Imagine a thermometer in your home. The temperature is state - it changes throughout the day, and your heating system watches it to decide when to turn on or off. That's exactly what state does in PicoFlow.
|
|
6
|
+
|
|
7
|
+
### Key Characteristics
|
|
8
|
+
|
|
9
|
+
- **Mutable**: Can be updated with `.set()`
|
|
10
|
+
- **Reactive**: Automatically notifies watchers on changes
|
|
11
|
+
- **Equality checking**: Only notifies if the new value differs from the current value
|
|
12
|
+
- **Type-safe**: TypeScript ensures updates match the initial type
|
|
13
|
+
|
|
14
|
+
## When to Use State
|
|
15
|
+
|
|
16
|
+
Use state when you need to:
|
|
17
|
+
|
|
18
|
+
- ✅ Track values that change over time
|
|
19
|
+
- ✅ Respond to user input or interactions
|
|
20
|
+
- ✅ Store data fetched from APIs
|
|
21
|
+
- ✅ Manage application state (UI state, form data, etc.)
|
|
22
|
+
|
|
23
|
+
Don't use state when:
|
|
24
|
+
|
|
25
|
+
- ❌ The value never changes (use constants instead)
|
|
26
|
+
- ❌ The value is derived from other reactive values (use derivations instead)
|
|
27
|
+
- ❌ You're tracking events without data (use signals instead)
|
|
28
|
+
|
|
29
|
+
## Creating State
|
|
30
|
+
|
|
31
|
+
Creating state is straightforward:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { state } from '@ersbeth/picoflow'
|
|
35
|
+
|
|
36
|
+
const $count = state(0)
|
|
37
|
+
const $name = state('Alice')
|
|
38
|
+
const $user = state({ id: 1, name: 'Bob', age: 25 })
|
|
39
|
+
const $items = state<string[]>([])
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The value you pass becomes the initial value. It can be any type: numbers, strings, objects, arrays, etc.
|
|
43
|
+
|
|
44
|
+
## Using State
|
|
45
|
+
|
|
46
|
+
State provides methods for reading, updating, and disposing.
|
|
47
|
+
|
|
48
|
+
### Reading with `.get(t)`
|
|
49
|
+
|
|
50
|
+
Inside an effect or derivation, use `.get(t)` to read the value and create a dependency:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { state, effect } from '@ersbeth/picoflow'
|
|
54
|
+
|
|
55
|
+
const $count = state(0)
|
|
56
|
+
|
|
57
|
+
effect((t) => {
|
|
58
|
+
const value = $count.get(t) // Read and track
|
|
59
|
+
console.log('Count is:', value)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
$count.set(1) // Console logs: "Count is: 1"
|
|
63
|
+
$count.set(2) // Console logs: "Count is: 2"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Reading with `.pick()`
|
|
67
|
+
|
|
68
|
+
Use `.pick()` when you want the current value without creating a dependency:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
const $count = state(0)
|
|
72
|
+
|
|
73
|
+
// Read without tracking (e.g., outside an effect)
|
|
74
|
+
const currentValue = $count.pick()
|
|
75
|
+
console.log(currentValue) // 0
|
|
76
|
+
|
|
77
|
+
// Or inside an effect, when you don't want to track changes
|
|
78
|
+
const $trigger = signal()
|
|
79
|
+
effect((t) => {
|
|
80
|
+
$trigger.watch(t) // Only track trigger
|
|
81
|
+
const snapshot = $count.pick() // Don't track count
|
|
82
|
+
console.log(snapshot)
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Updating with `.set(value)`
|
|
87
|
+
|
|
88
|
+
Pass the new value directly to `.set()`:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
const $count = state(0)
|
|
92
|
+
|
|
93
|
+
$count.set(5) // Set to 5
|
|
94
|
+
$count.set(10) // Set to 10
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Updating with `.set(updater)`
|
|
98
|
+
|
|
99
|
+
Pass a function that receives the current value and returns the new value:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
const $count = state(0)
|
|
103
|
+
|
|
104
|
+
$count.set(n => n + 1) // Increment by 1
|
|
105
|
+
$count.set(n => n * 2) // Double the value
|
|
106
|
+
$count.set(n => Math.max(0, n - 1)) // Decrement, min 0
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**When to use updater functions:**
|
|
110
|
+
- When the new value depends on the current value
|
|
111
|
+
- To guarantee you're working with the latest value
|
|
112
|
+
- For atomic updates (important in concurrent scenarios)
|
|
113
|
+
|
|
114
|
+
### Disposing with `.dispose()`
|
|
115
|
+
|
|
116
|
+
State can be disposed to free resources and prevent memory leaks:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
const $count = state(0)
|
|
120
|
+
|
|
121
|
+
// Use the state
|
|
122
|
+
effect((t) => {
|
|
123
|
+
console.log($count.get(t))
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// Later, clean up
|
|
127
|
+
$count.dispose()
|
|
128
|
+
|
|
129
|
+
// Subsequent operations will throw
|
|
130
|
+
$count.set(5) // Error: Primitive is disposed
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Lifecycle
|
|
134
|
+
|
|
135
|
+
When you update state, a specific flow occurs to ensure efficiency and consistency.
|
|
136
|
+
|
|
137
|
+
### Update Flow
|
|
138
|
+
|
|
139
|
+
Here's what happens when you call `.set()`:
|
|
140
|
+
|
|
141
|
+
```mermaid
|
|
142
|
+
sequenceDiagram
|
|
143
|
+
participant User
|
|
144
|
+
participant $count as $count (State)
|
|
145
|
+
participant $double as $double (Derivation)
|
|
146
|
+
participant Effect
|
|
147
|
+
|
|
148
|
+
User->>$count: set(n => n + 1)
|
|
149
|
+
activate $count
|
|
150
|
+
Note over $count: Get current: 0<br/>Compute new: 0 + 1 = 1
|
|
151
|
+
Note over $count: Check: 1 !== 0 ✓
|
|
152
|
+
Note over $count: Update: 0 → 1
|
|
153
|
+
|
|
154
|
+
$count->>$double: Notify (mark dirty)
|
|
155
|
+
activate $double
|
|
156
|
+
Note over $double: Mark as dirty<br/>NO recompute yet
|
|
157
|
+
deactivate $double
|
|
158
|
+
|
|
159
|
+
$count->>Effect: Schedule execution
|
|
160
|
+
deactivate $count
|
|
161
|
+
|
|
162
|
+
activate Effect
|
|
163
|
+
Note over Effect: Execute function
|
|
164
|
+
|
|
165
|
+
Effect->>$count: get(t)
|
|
166
|
+
activate $count
|
|
167
|
+
$count-->>Effect: 1
|
|
168
|
+
deactivate $count
|
|
169
|
+
|
|
170
|
+
Effect->>$double: get(t)
|
|
171
|
+
activate $double
|
|
172
|
+
Note over $double: Dirty? YES<br/>Recompute now
|
|
173
|
+
|
|
174
|
+
$double->>$count: get(t)
|
|
175
|
+
activate $count
|
|
176
|
+
$count-->>$double: 1
|
|
177
|
+
deactivate $count
|
|
178
|
+
|
|
179
|
+
Note over $double: Compute: 1 * 2 = 2
|
|
180
|
+
$double-->>Effect: 2
|
|
181
|
+
deactivate $double
|
|
182
|
+
|
|
183
|
+
Note over Effect: Log: "Count: 1, Double: 2"
|
|
184
|
+
deactivate Effect
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**How it works:**
|
|
188
|
+
|
|
189
|
+
1. **Compute new value**: If updater function, call it with current value
|
|
190
|
+
2. **Equality check**: Compare new value with current using `===`
|
|
191
|
+
3. **Early return**: If equal, skip notification entirely
|
|
192
|
+
4. **Update value**: Store the new value
|
|
193
|
+
5. **Notify dependents**:
|
|
194
|
+
- Derivations are marked dirty (not recomputed yet)
|
|
195
|
+
- Effects are scheduled for execution
|
|
196
|
+
6. **Pull-based recompute**: When effects run and read derivations, they recompute
|
|
197
|
+
|
|
198
|
+
### Equality Check Example
|
|
199
|
+
|
|
200
|
+
State only notifies when the value actually changes:
|
|
201
|
+
|
|
202
|
+
```mermaid
|
|
203
|
+
sequenceDiagram
|
|
204
|
+
participant User
|
|
205
|
+
participant $count as $count (State)
|
|
206
|
+
participant Effect
|
|
207
|
+
|
|
208
|
+
Note over User,Effect: Initial state: $count = 5
|
|
209
|
+
|
|
210
|
+
User->>$count: set(5)
|
|
211
|
+
activate $count
|
|
212
|
+
Note over $count: Check: 5 === 5?<br/>YES - no change
|
|
213
|
+
Note over $count: Skip notification
|
|
214
|
+
deactivate $count
|
|
215
|
+
Note over Effect: Effect does NOT run
|
|
216
|
+
|
|
217
|
+
User->>$count: set(10)
|
|
218
|
+
activate $count
|
|
219
|
+
Note over $count: Check: 10 === 5?<br/>NO - value changed
|
|
220
|
+
Note over $count: Update: 5 → 10
|
|
221
|
+
$count->>Effect: Notify & schedule
|
|
222
|
+
deactivate $count
|
|
223
|
+
|
|
224
|
+
activate Effect
|
|
225
|
+
Note over Effect: Effect runs
|
|
226
|
+
deactivate Effect
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Best Practices
|
|
230
|
+
|
|
231
|
+
### Use Updater Functions for Computed Updates
|
|
232
|
+
|
|
233
|
+
Always use updater functions when the new value depends on the current value:
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// ❌ Bad - race condition risk
|
|
237
|
+
const current = $count.pick()
|
|
238
|
+
$count.set(current + 1)
|
|
239
|
+
|
|
240
|
+
// ✅ Good - atomic update
|
|
241
|
+
$count.set(n => n + 1)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Immutable Updates for Objects
|
|
245
|
+
|
|
246
|
+
Create new objects instead of mutating:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
// ❌ Bad - mutation
|
|
250
|
+
$user.set(user => {
|
|
251
|
+
user.name = 'Bob' // Mutates the object!
|
|
252
|
+
return user
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
// ✅ Good - immutable
|
|
256
|
+
$user.set(user => ({
|
|
257
|
+
...user,
|
|
258
|
+
name: 'Bob'
|
|
259
|
+
}))
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Granular State Over Large Objects
|
|
263
|
+
|
|
264
|
+
Prefer multiple small state values when fields update independently:
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
// ❌ Less efficient - entire object tracked
|
|
268
|
+
const $form = state({
|
|
269
|
+
email: '',
|
|
270
|
+
password: '',
|
|
271
|
+
confirmPassword: ''
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
// ✅ More efficient - independent tracking
|
|
275
|
+
const form = {
|
|
276
|
+
$email: state(''),
|
|
277
|
+
$password = state(''),
|
|
278
|
+
$confirmPassword = state(''),
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Name State Clearly
|
|
283
|
+
|
|
284
|
+
Use the `$` prefix for all reactive values:
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
// ✅ Good - clear reactive naming
|
|
288
|
+
const $count = state(0)
|
|
289
|
+
const $user = state({...})
|
|
290
|
+
const $isLoading = state(false)
|
|
291
|
+
|
|
292
|
+
// ❌ Less clear
|
|
293
|
+
const count = state(0)
|
|
294
|
+
const user = state({...})
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Common Pitfalls
|
|
298
|
+
|
|
299
|
+
### Using `.pick()` in Effects
|
|
300
|
+
|
|
301
|
+
**Problem**: Using `.pick()` inside effects prevents reactivity.
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
// ❌ Wrong - will never update
|
|
305
|
+
effect((t) => {
|
|
306
|
+
const count = $count.pick() // No dependency created!
|
|
307
|
+
console.log(count) // Only logs once
|
|
308
|
+
})
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Solution**: Use `.get(t)` to create dependencies:
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
// ✅ Correct - creates dependency
|
|
315
|
+
effect((t) => {
|
|
316
|
+
const count = $count.get(t) // Creates dependency
|
|
317
|
+
console.log(count) // Logs on every change
|
|
318
|
+
})
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Forgetting Updater Functions
|
|
322
|
+
|
|
323
|
+
**Problem**: Reading and setting separately creates race conditions.
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
// ❌ Potential race condition
|
|
327
|
+
const current = $count.pick()
|
|
328
|
+
$count.set(current + 1)
|
|
329
|
+
// What if $count changes between pick() and set()?
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Solution**: Use updater functions:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
// ✅ Safe - uses latest value atomically
|
|
336
|
+
$count.set(n => n + 1)
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Mutating State Directly
|
|
340
|
+
|
|
341
|
+
**Problem**: Directly mutating arrays or objects doesn't trigger updates.
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
const $items = state([1, 2, 3])
|
|
345
|
+
|
|
346
|
+
// ❌ Wrong - mutates the array
|
|
347
|
+
const items = $items.pick()
|
|
348
|
+
items.push(4) // Doesn't trigger updates!
|
|
349
|
+
|
|
350
|
+
// ❌ Also wrong - same array reference
|
|
351
|
+
$items.set(items => {
|
|
352
|
+
items.push(4) // Mutation!
|
|
353
|
+
return items // Same reference, no notification!
|
|
354
|
+
})
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
**Solution**: Create new arrays/objects (or use FlowArray/FlowMap):
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
// ✅ Correct - creates new array
|
|
361
|
+
$items.set(items => [...items, 4])
|
|
362
|
+
|
|
363
|
+
// ✅ Also correct - for objects
|
|
364
|
+
$user.set(user => ({
|
|
365
|
+
...user,
|
|
366
|
+
age: user.age + 1
|
|
367
|
+
}))
|
|
368
|
+
```
|