preact-sigma 1.0.1 → 2.0.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 +113 -467
- package/dist/index.d.mts +207 -193
- package/dist/index.mjs +593 -265
- package/llms.txt +343 -292
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,509 +1,155 @@
|
|
|
1
|
-
#
|
|
1
|
+
# preact-sigma
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`preact-sigma` is a typed state-model builder for apps that want Preact's fine-grained reactivity, Immer-backed writes, and explicit lifecycle.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
You define a reusable state type once, then create instances wherever they make sense: inside components, in shared modules, or in plain TypeScript code. Each instance exposes readonly public state, tracked derived reads, imperative actions, and optional setup and event APIs.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## What It Is
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
pnpm add preact-sigma
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
```sh
|
|
14
|
-
npm install preact-sigma
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## Big Picture
|
|
18
|
-
|
|
19
|
-
Define state once, expose a few methods, and return reactive immutable data from the public instance.
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
import { computed, defineManagedState, type StateHandle } from "preact-sigma";
|
|
23
|
-
|
|
24
|
-
type CounterEvents = {
|
|
25
|
-
thresholdReached: [{ count: number }];
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
type CounterState = number;
|
|
29
|
-
|
|
30
|
-
const Counter = defineManagedState(
|
|
31
|
-
(counter: StateHandle<CounterState, CounterEvents>, step: number) => {
|
|
32
|
-
const doubled = computed(() => counter.get() * 2);
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
count: counter,
|
|
36
|
-
doubled,
|
|
37
|
-
increment() {
|
|
38
|
-
counter.set((value) => value + step);
|
|
39
|
-
|
|
40
|
-
if (counter.get() >= 10) {
|
|
41
|
-
counter.emit("thresholdReached", { count: counter.get() });
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
reset() {
|
|
45
|
-
counter.set(0);
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
},
|
|
49
|
-
0,
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
const counter = new Counter(2);
|
|
53
|
-
const stopThreshold = counter.on("thresholdReached", (event) => {
|
|
54
|
-
console.log(event.count);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
counter.count;
|
|
58
|
-
counter.doubled;
|
|
59
|
-
counter.increment();
|
|
60
|
-
stopThreshold();
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
- `count: counter` exposes the base state as a reactive immutable property.
|
|
64
|
-
- `doubled` is a memoized reactive value exposed through `computed()`.
|
|
65
|
-
- `increment()` is action-wrapped automatically, so the state update is batched and untracked.
|
|
66
|
-
- `counter.on(...)` returns `stopThreshold`, which unsubscribes the event listener.
|
|
67
|
-
|
|
68
|
-
## Define Reusable State
|
|
69
|
-
|
|
70
|
-
Use `defineManagedState()` when you want a reusable managed-state class.
|
|
71
|
-
|
|
72
|
-
```ts
|
|
73
|
-
import { defineManagedState, type StateHandle } from "preact-sigma";
|
|
74
|
-
|
|
75
|
-
type CounterState = number;
|
|
76
|
-
|
|
77
|
-
const Counter = defineManagedState(
|
|
78
|
-
(counter: StateHandle<CounterState>) => ({
|
|
79
|
-
count: counter,
|
|
80
|
-
increment() {
|
|
81
|
-
counter.set((value) => value + 1);
|
|
82
|
-
},
|
|
83
|
-
}),
|
|
84
|
-
0,
|
|
85
|
-
);
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## Expose Base State
|
|
89
|
-
|
|
90
|
-
Return the constructor handle when you want the base state to appear as a reactive immutable property.
|
|
91
|
-
|
|
92
|
-
```ts
|
|
93
|
-
import { defineManagedState, type StateHandle } from "preact-sigma";
|
|
94
|
-
|
|
95
|
-
type CounterState = number;
|
|
96
|
-
|
|
97
|
-
const Counter = defineManagedState(
|
|
98
|
-
(count: StateHandle<CounterState>) => ({
|
|
99
|
-
count,
|
|
100
|
-
}),
|
|
101
|
-
0,
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
new Counter().count;
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
## Memoize A Reactive Derivation
|
|
108
|
-
|
|
109
|
-
Use `computed()` when you want a memoized reactive value on the public instance.
|
|
110
|
-
|
|
111
|
-
```ts
|
|
112
|
-
import { computed, defineManagedState, type StateHandle } from "preact-sigma";
|
|
113
|
-
|
|
114
|
-
type CounterState = number;
|
|
115
|
-
|
|
116
|
-
const Counter = defineManagedState(
|
|
117
|
-
(counter: StateHandle<CounterState>) => ({
|
|
118
|
-
doubled: computed(() => counter.get() * 2),
|
|
119
|
-
}),
|
|
120
|
-
0,
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
new Counter().doubled;
|
|
124
|
-
```
|
|
9
|
+
At its core, `preact-sigma` lets you describe a stateful model as a constructor:
|
|
125
10
|
|
|
126
|
-
|
|
11
|
+
- top-level state stays reactive through one signal per state property
|
|
12
|
+
- computed values become tracked getters
|
|
13
|
+
- queries become tracked methods, including queries with arguments
|
|
14
|
+
- actions batch reads and writes through Immer drafts
|
|
15
|
+
- setup handlers own side effects and cleanup
|
|
16
|
+
- typed events let instances notify the outside world without exposing mutable internals
|
|
127
17
|
|
|
128
|
-
|
|
129
|
-
Query functions read from closed-over handles or signals and do not use
|
|
130
|
-
instance `this`.
|
|
18
|
+
The result feels like a small stateful object from application code, while still behaving like signal-driven state from rendering code.
|
|
131
19
|
|
|
132
|
-
|
|
133
|
-
import { defineManagedState, query, type StateHandle } from "preact-sigma";
|
|
134
|
-
|
|
135
|
-
type CounterState = number;
|
|
20
|
+
## What You Can Do With It
|
|
136
21
|
|
|
137
|
-
|
|
138
|
-
(counter: StateHandle<CounterState>) => ({
|
|
139
|
-
isPositive: query(() => counter.get() > 0),
|
|
140
|
-
}),
|
|
141
|
-
0,
|
|
142
|
-
);
|
|
22
|
+
`preact-sigma` is useful when you want state logic to live in one reusable unit instead of being split across loose signals, reducers, and effect cleanup code.
|
|
143
23
|
|
|
144
|
-
|
|
145
|
-
```
|
|
24
|
+
With it, you can:
|
|
146
25
|
|
|
147
|
-
|
|
26
|
+
- model domain state as reusable constructors instead of one-off store objects
|
|
27
|
+
- read public state directly while keeping writes inside typed action methods
|
|
28
|
+
- derive reactive values with computed getters and parameterized queries
|
|
29
|
+
- publish state changes from synchronous or async actions
|
|
30
|
+
- observe committed state changes and optional Immer patches
|
|
31
|
+
- snapshot committed top-level state and replace committed state for undo-like flows
|
|
32
|
+
- manage timers, listeners, nested state setup, and teardown through explicit cleanup
|
|
33
|
+
- use the same model inside Preact components with `useSigma(...)` and `useListener(...)`
|
|
148
34
|
|
|
149
|
-
|
|
35
|
+
## Why This Shape Exists
|
|
150
36
|
|
|
151
|
-
|
|
152
|
-
import { defineManagedState, type StateHandle } from "preact-sigma";
|
|
37
|
+
This package exists to keep stateful logic cohesive without giving up signal-level reactivity.
|
|
153
38
|
|
|
154
|
-
|
|
39
|
+
It is a good fit when plain signals start to sprawl across modules, but heavier store abstractions feel too opaque or too tied to component structure. `preact-sigma` keeps the "model object" ergonomics of a class-like API, while preserving readonly public reads, explicit write boundaries, and explicit ownership of side effects.
|
|
155
40
|
|
|
156
|
-
|
|
157
|
-
(counter: StateHandle<CounterState>) => ({
|
|
158
|
-
logNow() {
|
|
159
|
-
console.log(counter.peek());
|
|
160
|
-
},
|
|
161
|
-
}),
|
|
162
|
-
0,
|
|
163
|
-
);
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
## Use Top-Level Lenses
|
|
167
|
-
|
|
168
|
-
When the base state is object-shaped, the constructor handle exposes a shallow lens for each top-level property, and you can return that lens directly.
|
|
41
|
+
## Big Picture Example
|
|
169
42
|
|
|
170
43
|
```ts
|
|
171
|
-
import { computed,
|
|
44
|
+
import { computed, SigmaType } from "preact-sigma";
|
|
172
45
|
|
|
173
|
-
type
|
|
174
|
-
|
|
46
|
+
type Todo = {
|
|
47
|
+
id: string;
|
|
48
|
+
title: string;
|
|
49
|
+
done: boolean;
|
|
175
50
|
};
|
|
176
51
|
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
52
|
+
const TodoList = new SigmaType<
|
|
53
|
+
{ draft: string; todos: Todo[]; saving: boolean },
|
|
54
|
+
{ saved: { count: number } }
|
|
55
|
+
>("TodoList")
|
|
56
|
+
.defaultState({
|
|
57
|
+
draft: "",
|
|
58
|
+
todos: [],
|
|
59
|
+
saving: false,
|
|
60
|
+
})
|
|
61
|
+
.computed({
|
|
62
|
+
// Computeds are tracked getters with no arguments.
|
|
63
|
+
remainingCount() {
|
|
64
|
+
return this.todos.filter((todo) => !todo.done).length;
|
|
183
65
|
},
|
|
184
|
-
})
|
|
185
|
-
{
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
When the base state is object-shaped, spread the handle into the returned object to expose its current top-level lenses at once.
|
|
194
|
-
|
|
195
|
-
```ts
|
|
196
|
-
import { defineManagedState, type StateHandle } from "preact-sigma";
|
|
197
|
-
|
|
198
|
-
type SearchState = {
|
|
199
|
-
page: number;
|
|
200
|
-
query: string;
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
const Search = defineManagedState(
|
|
204
|
-
(search: StateHandle<SearchState>) => ({
|
|
205
|
-
...search,
|
|
206
|
-
nextPage() {
|
|
207
|
-
search.page.set((page) => page + 1);
|
|
208
|
-
},
|
|
209
|
-
}),
|
|
210
|
-
{ page: 1, query: "" },
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
const search = new Search();
|
|
214
|
-
|
|
215
|
-
search.query;
|
|
216
|
-
search.page;
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
## Compose Managed States
|
|
220
|
-
|
|
221
|
-
Return another managed-state instance when you want to expose it unchanged as a property.
|
|
222
|
-
Composed managed states are available through direct property access and whole-state snapshots.
|
|
223
|
-
|
|
224
|
-
```ts
|
|
225
|
-
import { defineManagedState, type StateHandle } from "preact-sigma";
|
|
226
|
-
|
|
227
|
-
type CounterState = number;
|
|
228
|
-
|
|
229
|
-
const Counter = defineManagedState(
|
|
230
|
-
(count: StateHandle<CounterState>) => ({
|
|
231
|
-
count,
|
|
232
|
-
increment() {
|
|
233
|
-
count.set((value) => value + 1);
|
|
66
|
+
})
|
|
67
|
+
.queries({
|
|
68
|
+
// Queries stay reactive at the call site and can accept arguments.
|
|
69
|
+
visibleTodos(filter: "all" | "open" | "done") {
|
|
70
|
+
return this.todos.filter((todo) => {
|
|
71
|
+
if (filter === "open") return !todo.done;
|
|
72
|
+
if (filter === "done") return todo.done;
|
|
73
|
+
return true;
|
|
74
|
+
});
|
|
234
75
|
},
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
ready: boolean;
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const Dashboard = defineManagedState(
|
|
244
|
-
(dashboard: StateHandle<DashboardState>) => ({
|
|
245
|
-
dashboard,
|
|
246
|
-
counter: new Counter(),
|
|
247
|
-
}),
|
|
248
|
-
{ ready: false },
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
new Dashboard().counter.increment();
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
## Own Resources And Dispose The Instance
|
|
255
|
-
|
|
256
|
-
Use `handle.own()` to register cleanup functions or disposables, and call `.dispose()` when the managed state should release them.
|
|
257
|
-
|
|
258
|
-
```ts
|
|
259
|
-
import { defineManagedState, type StateHandle } from "preact-sigma";
|
|
260
|
-
|
|
261
|
-
type PollerState = {
|
|
262
|
-
ticks: number;
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
const Poller = defineManagedState(
|
|
266
|
-
(poller: StateHandle<PollerState>) => ({
|
|
267
|
-
ticks: poller.ticks,
|
|
268
|
-
start() {
|
|
269
|
-
const interval = window.setInterval(() => {
|
|
270
|
-
poller.ticks.set((ticks) => ticks + 1);
|
|
271
|
-
}, 1000);
|
|
272
|
-
|
|
273
|
-
poller.own([() => window.clearInterval(interval)]);
|
|
76
|
+
})
|
|
77
|
+
.actions({
|
|
78
|
+
// Public state is readonly, so writes live in actions.
|
|
79
|
+
setDraft(draft: string) {
|
|
80
|
+
this.draft = draft;
|
|
274
81
|
},
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
const poller = new Poller();
|
|
82
|
+
addTodo() {
|
|
83
|
+
if (!this.draft.trim()) return;
|
|
280
84
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
## Update State
|
|
286
|
-
|
|
287
|
-
Pass an Immer producer to `.set()` when your base state is object-shaped.
|
|
288
|
-
|
|
289
|
-
```ts
|
|
290
|
-
import { defineManagedState, type StateHandle } from "preact-sigma";
|
|
291
|
-
|
|
292
|
-
type SearchState = {
|
|
293
|
-
query: string;
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
const Search = defineManagedState(
|
|
297
|
-
(search: StateHandle<SearchState>) => ({
|
|
298
|
-
setQuery(query: string) {
|
|
299
|
-
search.set((draft) => {
|
|
300
|
-
draft.query = query;
|
|
85
|
+
this.todos.push({
|
|
86
|
+
id: crypto.randomUUID(),
|
|
87
|
+
title: this.draft,
|
|
88
|
+
done: false,
|
|
301
89
|
});
|
|
90
|
+
this.draft = "";
|
|
302
91
|
},
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
);
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
## Emit Events
|
|
309
|
-
|
|
310
|
-
Use `.emit()` to publish a custom event with zero or one argument.
|
|
311
|
-
|
|
312
|
-
```ts
|
|
313
|
-
import { defineManagedState, type StateHandle } from "preact-sigma";
|
|
314
|
-
|
|
315
|
-
type TodoEvents = {
|
|
316
|
-
saved: [];
|
|
317
|
-
selected: [{ id: string }];
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
type TodoState = {};
|
|
321
|
-
|
|
322
|
-
const Todo = defineManagedState(
|
|
323
|
-
(todo: StateHandle<TodoState, TodoEvents>) => ({
|
|
324
|
-
save() {
|
|
325
|
-
todo.emit("saved");
|
|
326
|
-
},
|
|
327
|
-
select(id: string) {
|
|
328
|
-
todo.emit("selected", { id });
|
|
92
|
+
toggleTodo(id: string) {
|
|
93
|
+
const todo = this.todos.find((todo) => todo.id === id);
|
|
94
|
+
if (todo) todo.done = !todo.done;
|
|
329
95
|
},
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
);
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
## Listen For Events
|
|
336
|
-
|
|
337
|
-
Use `.on()` to subscribe to custom events from a managed state instance.
|
|
338
|
-
|
|
339
|
-
```ts
|
|
340
|
-
const todo = new Todo();
|
|
341
|
-
|
|
342
|
-
const stopSaved = todo.on("saved", () => {
|
|
343
|
-
console.log("saved");
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
const stopSelected = todo.on("selected", (event) => {
|
|
347
|
-
console.log(event.id);
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
stopSaved();
|
|
351
|
-
stopSelected();
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
## Read Signals From A Managed State
|
|
355
|
-
|
|
356
|
-
Use `.get(key)` for one exposed signal-backed property or `.get()` for the whole public state signal.
|
|
357
|
-
Keyed reads do not target composed managed-state properties.
|
|
358
|
-
|
|
359
|
-
```ts
|
|
360
|
-
const counter = new Counter();
|
|
361
|
-
|
|
362
|
-
const countSignal = counter.get("count");
|
|
363
|
-
const counterSignal = counter.get();
|
|
364
|
-
|
|
365
|
-
countSignal.value;
|
|
366
|
-
counterSignal.value.count;
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
## Peek At Public State
|
|
370
|
-
|
|
371
|
-
Use `.peek(key)` for one exposed signal-backed property or `.peek()` for the whole public snapshot.
|
|
372
|
-
Keyed peeks do not target composed managed-state properties.
|
|
373
|
-
|
|
374
|
-
```ts
|
|
375
|
-
const counter = new Counter();
|
|
376
|
-
|
|
377
|
-
counter.peek("count");
|
|
378
|
-
counter.peek();
|
|
379
|
-
```
|
|
96
|
+
async save() {
|
|
97
|
+
this.saving = true;
|
|
98
|
+
this.commit(); // Publish the loading state before awaiting.
|
|
380
99
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
Keyed subscriptions do not target composed managed-state properties.
|
|
386
|
-
|
|
387
|
-
```ts
|
|
388
|
-
const counter = new Counter();
|
|
389
|
-
|
|
390
|
-
const stopCount = counter.subscribe("count", (count) => {
|
|
391
|
-
console.log(count);
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
const stopState = counter.subscribe((value) => {
|
|
395
|
-
console.log(value.count);
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
stopCount();
|
|
399
|
-
stopState();
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
## Use It Inside A Component
|
|
403
|
-
|
|
404
|
-
Use `useManagedState()` when you want the same pattern directly inside a component.
|
|
405
|
-
|
|
406
|
-
```tsx
|
|
407
|
-
import { useManagedState, type StateHandle } from "preact-sigma";
|
|
408
|
-
|
|
409
|
-
type SearchState = {
|
|
410
|
-
query: string;
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
function SearchBox() {
|
|
414
|
-
const search = useManagedState(
|
|
415
|
-
(search: StateHandle<SearchState>) => ({
|
|
416
|
-
query: search.query,
|
|
417
|
-
setQuery(query: string) {
|
|
418
|
-
search.query.set(query);
|
|
419
|
-
},
|
|
420
|
-
}),
|
|
421
|
-
() => ({ query: "" }),
|
|
422
|
-
);
|
|
423
|
-
|
|
424
|
-
return (
|
|
425
|
-
<input value={search.query} onInput={(event) => search.setQuery(event.currentTarget.value)} />
|
|
426
|
-
);
|
|
427
|
-
}
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
## Subscribe In `useEffect`
|
|
431
|
-
|
|
432
|
-
Use `useSubscribe()` with any subscribable source, including managed state and Preact signals.
|
|
433
|
-
The listener receives the current value immediately and then future updates.
|
|
434
|
-
|
|
435
|
-
```tsx
|
|
436
|
-
import { useSubscribe } from "preact-sigma";
|
|
437
|
-
|
|
438
|
-
useSubscribe(counter, (value) => {
|
|
439
|
-
console.log(value.count);
|
|
440
|
-
});
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
## Listen To DOM Or Managed-State Events In `useEffect`
|
|
444
|
-
|
|
445
|
-
Use `useEventTarget()` for either DOM events or managed-state events.
|
|
446
|
-
|
|
447
|
-
```tsx
|
|
448
|
-
import { useEventTarget } from "preact-sigma";
|
|
449
|
-
|
|
450
|
-
useEventTarget(window, "resize", () => {
|
|
451
|
-
console.log(window.innerWidth);
|
|
452
|
-
});
|
|
453
|
-
```
|
|
100
|
+
await fetch("/api/todos", {
|
|
101
|
+
method: "POST",
|
|
102
|
+
body: JSON.stringify(this.todos),
|
|
103
|
+
});
|
|
454
104
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
|
|
105
|
+
this.saving = false;
|
|
106
|
+
this.commit(); // Publish post-await writes explicitly.
|
|
107
|
+
this.emit("saved", { count: this.todos.length });
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
.setup(function (storageKey: string) {
|
|
111
|
+
// Setup is explicit and returns cleanup resources.
|
|
112
|
+
const interval = window.setInterval(() => {
|
|
113
|
+
localStorage.setItem(storageKey, JSON.stringify(this.todos));
|
|
114
|
+
}, 1000);
|
|
460
115
|
|
|
461
|
-
|
|
116
|
+
return [() => window.clearInterval(interval)];
|
|
117
|
+
});
|
|
462
118
|
|
|
463
|
-
|
|
119
|
+
const todoList = new TodoList();
|
|
464
120
|
|
|
465
|
-
|
|
466
|
-
|
|
121
|
+
// setup(...) returns one cleanup function for everything this instance owns.
|
|
122
|
+
const cleanup = todoList.setup("todos-demo");
|
|
467
123
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
124
|
+
// Queries are reactive where they are read.
|
|
125
|
+
const firstOpenTitle = computed(() => {
|
|
126
|
+
return todoList.visibleTodos("open")[0]?.title ?? "Nothing open";
|
|
471
127
|
});
|
|
472
128
|
|
|
473
|
-
|
|
474
|
-
|
|
129
|
+
// Events are typed and unsubscribe cleanly.
|
|
130
|
+
const stop = todoList.on("saved", ({ count }) => {
|
|
131
|
+
console.log(`Saved ${count} todos`);
|
|
475
132
|
});
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
## Small Feature Model
|
|
479
|
-
|
|
480
|
-
This pattern works well when a component or UI feature needs a small state model with a few public methods and derived values.
|
|
481
133
|
|
|
482
|
-
|
|
483
|
-
|
|
134
|
+
todoList.setDraft("Write the README");
|
|
135
|
+
todoList.addTodo();
|
|
136
|
+
await todoList.save();
|
|
484
137
|
|
|
485
|
-
|
|
138
|
+
console.log(todoList.remainingCount);
|
|
139
|
+
console.log(firstOpenTitle.value);
|
|
486
140
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
open: dialog,
|
|
490
|
-
show() {
|
|
491
|
-
dialog.set(true);
|
|
492
|
-
},
|
|
493
|
-
hide() {
|
|
494
|
-
dialog.set(false);
|
|
495
|
-
},
|
|
496
|
-
}),
|
|
497
|
-
false,
|
|
498
|
-
);
|
|
141
|
+
stop();
|
|
142
|
+
cleanup();
|
|
499
143
|
```
|
|
500
144
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
```tsx
|
|
504
|
-
const [open, setOpen] = useState(false);
|
|
505
|
-
```
|
|
145
|
+
In Preact, the same constructor can be used with `useSigma(() => new TodoList(), ["todos-demo"])` so the component owns one instance and `setup(...)` cleanup runs automatically. Use `useListener(...)` when you want component-scoped event subscriptions with automatic teardown.
|
|
506
146
|
|
|
507
|
-
##
|
|
147
|
+
## Best Practices
|
|
508
148
|
|
|
509
|
-
|
|
149
|
+
- Let `new SigmaType<TState, TEvents>()` and the builder inputs drive inference. Avoid forcing extra type arguments onto builder methods.
|
|
150
|
+
- Keep top-level state properties meaningful. Each top-level property gets its own signal, so shape state around the reads you want to track.
|
|
151
|
+
- Use `computed(...)` for argument-free derived state, and use queries for reactive reads that need parameters.
|
|
152
|
+
- Put writes in actions. A draft boundary is any point where sigma cannot keep reusing the current draft. `emit()`, `await`, and any action call other than a same-instance sync nested action call are draft boundaries, so call `this.commit()` before those boundaries when pending writes should become public.
|
|
153
|
+
- Use `snapshot(instance)` and `replaceState(instance, snapshot)` for committed-state replay. They work on top-level state keys and stay outside action semantics.
|
|
154
|
+
- Use `immerable` on custom classes only when they should participate in Immer drafting. `setAutoFreeze(false)` disables sigma's runtime deep-freezing when you need published state to stay unfrozen.
|
|
155
|
+
- Use `setup(...)` for owned side effects, and always return cleanup resources for anything the instance starts.
|