preact-sigma 2.2.1 → 2.2.2
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 +189 -58
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,23 +1,56 @@
|
|
|
1
1
|
# preact-sigma
|
|
2
2
|
|
|
3
|
-
`preact-sigma`
|
|
3
|
+
`preact-sigma` lets you define state once and reuse it as a model.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
It is built for Preact and TypeScript, and combines:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- fine-grained reactive reads
|
|
8
|
+
- Immer-style writes
|
|
9
|
+
- explicit setup and cleanup
|
|
10
|
+
- typed events
|
|
11
|
+
- a constructor you can instantiate anywhere
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
Use it when your state has started to feel like more than "some values in a component."
|
|
14
|
+
|
|
15
|
+
Instead of spreading logic across loose signals, reducers, effects, and cleanup code, you define one model with:
|
|
16
|
+
|
|
17
|
+
- top-level state
|
|
18
|
+
- derived reads
|
|
19
|
+
- write methods
|
|
20
|
+
- side-effect setup
|
|
21
|
+
- optional events
|
|
22
|
+
|
|
23
|
+
Then you create instances wherever they make sense: inside a component, in a shared module, or in plain TypeScript.
|
|
24
|
+
|
|
25
|
+
Under the hood, each top-level state property is backed by its own Preact signal, while writes happen through actions with Immer-backed mutation semantics.
|
|
26
|
+
|
|
27
|
+
## Why you would use it
|
|
28
|
+
|
|
29
|
+
`preact-sigma` is a good fit when you want state and behavior to live together.
|
|
30
|
+
|
|
31
|
+
It is especially useful when you need to:
|
|
32
|
+
|
|
33
|
+
- keep state, derived values, mutations, and lifecycle in one place
|
|
34
|
+
- create multiple instances of the same state model
|
|
35
|
+
- expose readonly public state while keeping writes explicit
|
|
36
|
+
- get fine-grained reactivity without wiring together a pile of loose signals
|
|
37
|
+
- own timers, subscriptions, listeners, or nested setup with clear cleanup
|
|
38
|
+
|
|
39
|
+
If a couple of plain signals are enough, use plain signals.
|
|
40
|
+
`preact-sigma` is for the point where state starts acting like a small system.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
10
43
|
|
|
11
44
|
```bash
|
|
12
45
|
npm install preact-sigma
|
|
13
46
|
```
|
|
14
47
|
|
|
15
|
-
##
|
|
48
|
+
## 30-second example
|
|
16
49
|
|
|
17
50
|
```ts
|
|
18
51
|
import { SigmaType } from "preact-sigma";
|
|
19
52
|
|
|
20
|
-
const Counter = new SigmaType<{ count: number }>(
|
|
53
|
+
const Counter = new SigmaType<{ count: number }>()
|
|
21
54
|
.defaultState({
|
|
22
55
|
count: 0,
|
|
23
56
|
})
|
|
@@ -40,44 +73,96 @@ console.log(counter.count); // 1
|
|
|
40
73
|
console.log(counter.doubled); // 2
|
|
41
74
|
```
|
|
42
75
|
|
|
43
|
-
|
|
76
|
+
That example shows the basic shape:
|
|
77
|
+
|
|
78
|
+
- state is public and reactive
|
|
79
|
+
- derived values live in `computed(...)`
|
|
80
|
+
- writes happen in `actions(...)`
|
|
81
|
+
- an instance behaves like a small stateful object
|
|
82
|
+
|
|
83
|
+
## The mental model
|
|
84
|
+
|
|
85
|
+
A sigma model is made from a few simple pieces.
|
|
86
|
+
|
|
87
|
+
### `defaultState(...)`
|
|
88
|
+
|
|
89
|
+
Defines the top-level state for each instance.
|
|
90
|
+
|
|
91
|
+
Each top-level property becomes a reactive public property on the instance.
|
|
92
|
+
|
|
93
|
+
Use plain values for simple defaults, or zero-argument functions when each instance needs a fresh object or array.
|
|
94
|
+
|
|
95
|
+
### `computed(...)`
|
|
96
|
+
|
|
97
|
+
Use computeds for derived values that take no arguments.
|
|
98
|
+
|
|
99
|
+
They behave like tracked getters:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
.completedCount() // no
|
|
103
|
+
todoList.completedCount // yes
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `queries(...)`
|
|
44
107
|
|
|
45
|
-
|
|
108
|
+
Use queries for reactive reads that need arguments.
|
|
46
109
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
- typed events let instances notify the outside world without exposing mutable internals
|
|
110
|
+
```ts
|
|
111
|
+
visibleTodos("open");
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Queries are for reading, not writing.
|
|
53
115
|
|
|
54
|
-
|
|
116
|
+
### `actions(...)`
|
|
55
117
|
|
|
56
|
-
|
|
118
|
+
Actions are where state changes happen.
|
|
57
119
|
|
|
58
|
-
|
|
120
|
+
Outside an action, public state is readonly. Inside an action, you write with normal mutation syntax and sigma handles the draft/update flow for you.
|
|
59
121
|
|
|
60
|
-
|
|
122
|
+
```ts
|
|
123
|
+
.actions({
|
|
124
|
+
rename(title: string) {
|
|
125
|
+
this.title = title;
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
```
|
|
61
129
|
|
|
62
|
-
|
|
63
|
-
- read public state directly while keeping writes inside typed action methods
|
|
64
|
-
- derive reactive values with computed getters and parameterized queries
|
|
65
|
-
- publish state changes from synchronous or async actions
|
|
66
|
-
- observe committed state changes and optional Immer patches
|
|
67
|
-
- snapshot committed top-level state and replace committed state for undo-like flows
|
|
68
|
-
- manage timers, listeners, nested state setup, and teardown through explicit cleanup
|
|
69
|
-
- use the same model inside Preact components with `useSigma(...)` and `useListener(...)`
|
|
130
|
+
### `setup(...)`
|
|
70
131
|
|
|
71
|
-
|
|
132
|
+
Setup is where side effects belong.
|
|
72
133
|
|
|
73
|
-
|
|
134
|
+
Use it for things like:
|
|
74
135
|
|
|
75
|
-
|
|
136
|
+
- timers
|
|
137
|
+
- event listeners
|
|
138
|
+
- subscriptions
|
|
139
|
+
- nested model setup
|
|
140
|
+
- storage sync
|
|
76
141
|
|
|
77
|
-
|
|
142
|
+
Setup is explicit. A new instance does not automatically run setup. When setup does run, it returns one cleanup function that tears down everything that instance owns.
|
|
143
|
+
|
|
144
|
+
### Events
|
|
145
|
+
|
|
146
|
+
Use events when the model needs to notify the outside world without exposing mutable internals.
|
|
147
|
+
|
|
148
|
+
Emit inside actions or setup:
|
|
78
149
|
|
|
79
150
|
```ts
|
|
80
|
-
|
|
151
|
+
this.emit("saved", { count: 3 });
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Listen from the outside:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const stop = instance.on("saved", ({ count }) => {
|
|
158
|
+
console.log(count);
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## A more realistic example
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { SigmaType } from "preact-sigma";
|
|
81
166
|
|
|
82
167
|
type Todo = {
|
|
83
168
|
id: string;
|
|
@@ -88,20 +173,18 @@ type Todo = {
|
|
|
88
173
|
const TodoList = new SigmaType<
|
|
89
174
|
{ draft: string; todos: Todo[]; saving: boolean },
|
|
90
175
|
{ saved: { count: number } }
|
|
91
|
-
>(
|
|
176
|
+
>()
|
|
92
177
|
.defaultState({
|
|
93
178
|
draft: "",
|
|
94
179
|
todos: [],
|
|
95
180
|
saving: false,
|
|
96
181
|
})
|
|
97
182
|
.computed({
|
|
98
|
-
// Computeds are tracked getters with no arguments.
|
|
99
183
|
remainingCount() {
|
|
100
184
|
return this.todos.filter((todo) => !todo.done).length;
|
|
101
185
|
},
|
|
102
186
|
})
|
|
103
187
|
.queries({
|
|
104
|
-
// Queries stay reactive at the call site and can accept arguments.
|
|
105
188
|
visibleTodos(filter: "all" | "open" | "done") {
|
|
106
189
|
return this.todos.filter((todo) => {
|
|
107
190
|
if (filter === "open") return !todo.done;
|
|
@@ -111,7 +194,6 @@ const TodoList = new SigmaType<
|
|
|
111
194
|
},
|
|
112
195
|
})
|
|
113
196
|
.actions({
|
|
114
|
-
// Public state is readonly, so writes live in actions.
|
|
115
197
|
setDraft(draft: string) {
|
|
116
198
|
this.draft = draft;
|
|
117
199
|
},
|
|
@@ -123,6 +205,7 @@ const TodoList = new SigmaType<
|
|
|
123
205
|
title: this.draft,
|
|
124
206
|
done: false,
|
|
125
207
|
});
|
|
208
|
+
|
|
126
209
|
this.draft = "";
|
|
127
210
|
},
|
|
128
211
|
toggleTodo(id: string) {
|
|
@@ -131,7 +214,7 @@ const TodoList = new SigmaType<
|
|
|
131
214
|
},
|
|
132
215
|
async save() {
|
|
133
216
|
this.saving = true;
|
|
134
|
-
this.commit(); //
|
|
217
|
+
this.commit(); // publish "saving" before awaiting
|
|
135
218
|
|
|
136
219
|
await fetch("/api/todos", {
|
|
137
220
|
method: "POST",
|
|
@@ -139,12 +222,11 @@ const TodoList = new SigmaType<
|
|
|
139
222
|
});
|
|
140
223
|
|
|
141
224
|
this.saving = false;
|
|
142
|
-
this.commit(); //
|
|
225
|
+
this.commit(); // publish before emitting
|
|
143
226
|
this.emit("saved", { count: this.todos.length });
|
|
144
227
|
},
|
|
145
228
|
})
|
|
146
229
|
.setup(function (storageKey: string) {
|
|
147
|
-
// Setup is explicit and returns cleanup resources.
|
|
148
230
|
const interval = window.setInterval(() => {
|
|
149
231
|
localStorage.setItem(storageKey, JSON.stringify(this.todos));
|
|
150
232
|
}, 1000);
|
|
@@ -153,44 +235,93 @@ const TodoList = new SigmaType<
|
|
|
153
235
|
});
|
|
154
236
|
|
|
155
237
|
const todoList = new TodoList();
|
|
156
|
-
|
|
157
|
-
// setup(...) returns one cleanup function for everything this instance owns.
|
|
158
238
|
const cleanup = todoList.setup("todos-demo");
|
|
159
239
|
|
|
160
|
-
// Queries are reactive where they are read.
|
|
161
|
-
const firstOpenTitle = computed(() => {
|
|
162
|
-
return todoList.visibleTodos("open")[0]?.title ?? "Nothing open";
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// Events are typed and unsubscribe cleanly.
|
|
166
240
|
const stop = todoList.on("saved", ({ count }) => {
|
|
167
241
|
console.log(`Saved ${count} todos`);
|
|
168
242
|
});
|
|
169
243
|
|
|
170
|
-
todoList.setDraft("
|
|
244
|
+
todoList.setDraft("Rewrite the README");
|
|
171
245
|
todoList.addTodo();
|
|
172
|
-
await todoList.save();
|
|
173
246
|
|
|
174
247
|
console.log(todoList.remainingCount);
|
|
175
|
-
console.log(
|
|
248
|
+
console.log(todoList.visibleTodos("open"));
|
|
249
|
+
|
|
250
|
+
await todoList.save();
|
|
176
251
|
|
|
177
252
|
stop();
|
|
178
253
|
cleanup();
|
|
179
254
|
```
|
|
180
255
|
|
|
181
|
-
|
|
256
|
+
## The one rule to remember about actions
|
|
257
|
+
|
|
258
|
+
For normal synchronous actions, mutate state and return. You usually do **not** need `this.commit()`.
|
|
259
|
+
|
|
260
|
+
Use `this.commit()` when you have unpublished changes and the action is about to cross a boundary like:
|
|
261
|
+
|
|
262
|
+
- `await`
|
|
263
|
+
- `emit(...)`
|
|
264
|
+
- another action boundary that should not keep using the current draft
|
|
265
|
+
|
|
266
|
+
In practice, that means:
|
|
267
|
+
|
|
268
|
+
- sync action with no boundary: mutate and return
|
|
269
|
+
- async action before `await`: `commit()` if you want those changes published first
|
|
270
|
+
- action before `emit(...)`: `commit()` if there are pending changes
|
|
271
|
+
|
|
272
|
+
That rule is the main thing to learn beyond the basic API.
|
|
273
|
+
|
|
274
|
+
## In Preact
|
|
275
|
+
|
|
276
|
+
`preact-sigma` works outside components, but it also has a nice component story.
|
|
277
|
+
|
|
278
|
+
Use `useSigma(...)` when the component should own one instance:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
import { useSigma } from "preact-sigma";
|
|
282
|
+
|
|
283
|
+
const todoList = useSigma(() => new TodoList(), ["todos-demo"]);
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
If the model defines setup handlers, `useSigma(...)` runs setup for that component-owned instance and cleans it up automatically when setup params change or the component unmounts.
|
|
287
|
+
|
|
288
|
+
Use `useListener(...)` for component-scoped event subscriptions:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
import { useListener } from "preact-sigma";
|
|
292
|
+
|
|
293
|
+
useListener(todoList, "saved", ({ count }) => {
|
|
294
|
+
console.log(`Saved ${count} todos`);
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## What you get out of the box
|
|
299
|
+
|
|
300
|
+
Beyond the core model API, `preact-sigma` also includes:
|
|
301
|
+
|
|
302
|
+
- `observe(...)` for reacting to committed state changes
|
|
303
|
+
- optional Immer patch delivery in observers
|
|
304
|
+
- `snapshot(...)` and `replaceState(...)` for restore/undo-like flows
|
|
305
|
+
- `get(key)` when you need direct signal access for a state key or computed
|
|
306
|
+
|
|
307
|
+
## Why this shape exists
|
|
308
|
+
|
|
309
|
+
`preact-sigma` exists for the space between two extremes:
|
|
182
310
|
|
|
183
|
-
|
|
311
|
+
- **too small for a big store abstraction**
|
|
312
|
+
- **too stateful for a handful of loose signals**
|
|
184
313
|
|
|
185
|
-
|
|
314
|
+
It keeps the ergonomics of working with a model object, while preserving:
|
|
186
315
|
|
|
187
|
-
|
|
316
|
+
- readonly public reads
|
|
317
|
+
- explicit write boundaries
|
|
318
|
+
- fine-grained reactivity
|
|
319
|
+
- explicit ownership of side effects
|
|
188
320
|
|
|
189
|
-
|
|
190
|
-
- Constructor input shallowly overrides `defaultState`, so `new TodoList({ draft: "ready" })` replaces only the top-level keys you pass.
|
|
321
|
+
That makes it useful for app state that has real behavior, not just values.
|
|
191
322
|
|
|
192
|
-
## More
|
|
323
|
+
## More docs
|
|
193
324
|
|
|
194
|
-
- [llms.txt](./llms.txt)
|
|
325
|
+
- [`llms.txt`](./llms.txt) contains the exhaustive API and behavior reference.
|
|
195
326
|
- Companion skills are available via `npx skills add alloc/preact-sigma`.
|
|
196
|
-
- The `preact-sigma` skill packages
|
|
327
|
+
- The `preact-sigma` skill packages procedural guidance and agent-oriented workflow for the library.
|