preact-sigma 0.0.0 → 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/README.md +509 -0
- package/dist/index.d.mts +215 -0
- package/dist/index.mjs +301 -0
- package/llms.txt +367 -0
- package/package.json +43 -9
- package/readme.md +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
# `preact-sigma`
|
|
2
|
+
|
|
3
|
+
Managed UI state for Preact Signals, with Immer-powered updates and a small public API.
|
|
4
|
+
|
|
5
|
+
For naming and API design conventions, see [best-practices.md](./best-practices.md).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
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
|
+
```
|
|
125
|
+
|
|
126
|
+
## Create A Tracked Query Method
|
|
127
|
+
|
|
128
|
+
Use `query()` when you want a public method whose reads stay tracked.
|
|
129
|
+
Query functions read from closed-over handles or signals and do not use
|
|
130
|
+
instance `this`.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { defineManagedState, query, type StateHandle } from "preact-sigma";
|
|
134
|
+
|
|
135
|
+
type CounterState = number;
|
|
136
|
+
|
|
137
|
+
const Counter = defineManagedState(
|
|
138
|
+
(counter: StateHandle<CounterState>) => ({
|
|
139
|
+
isPositive: query(() => counter.get() > 0),
|
|
140
|
+
}),
|
|
141
|
+
0,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
new Counter().isPositive();
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Read Base State Without Tracking
|
|
148
|
+
|
|
149
|
+
Use `handle.peek()` when you need the current base-state snapshot without creating a reactive dependency.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { defineManagedState, type StateHandle } from "preact-sigma";
|
|
153
|
+
|
|
154
|
+
type CounterState = number;
|
|
155
|
+
|
|
156
|
+
const Counter = defineManagedState(
|
|
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.
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import { computed, defineManagedState, type StateHandle } from "preact-sigma";
|
|
172
|
+
|
|
173
|
+
type SearchState = {
|
|
174
|
+
query: string;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const Search = defineManagedState(
|
|
178
|
+
(search: StateHandle<SearchState>) => ({
|
|
179
|
+
query: search.query,
|
|
180
|
+
trimmedQuery: computed(() => search.query.get().trim()),
|
|
181
|
+
setQuery(query: string) {
|
|
182
|
+
search.query.set(query);
|
|
183
|
+
},
|
|
184
|
+
}),
|
|
185
|
+
{ query: "" },
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
new Search().query;
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Spread A Handle To Expose Top-Level Properties
|
|
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);
|
|
234
|
+
},
|
|
235
|
+
}),
|
|
236
|
+
0,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
type DashboardState = {
|
|
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)]);
|
|
274
|
+
},
|
|
275
|
+
}),
|
|
276
|
+
{ ticks: 0 },
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const poller = new Poller();
|
|
280
|
+
|
|
281
|
+
poller.start();
|
|
282
|
+
poller.dispose();
|
|
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;
|
|
301
|
+
});
|
|
302
|
+
},
|
|
303
|
+
}),
|
|
304
|
+
{ query: "" },
|
|
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 });
|
|
329
|
+
},
|
|
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
|
+
```
|
|
380
|
+
|
|
381
|
+
## Subscribe To Public State
|
|
382
|
+
|
|
383
|
+
Use `.subscribe(key, listener)` for one exposed signal-backed property or `.subscribe(listener)` for the whole public state.
|
|
384
|
+
Listeners receive the current value immediately and then future updates.
|
|
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
|
+
```
|
|
454
|
+
|
|
455
|
+
```tsx
|
|
456
|
+
useEventTarget(counter, "thresholdReached", (event) => {
|
|
457
|
+
console.log(event.count);
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
## Reach For Signals Helpers
|
|
462
|
+
|
|
463
|
+
`batch` and `untracked` are re-exported from `@preact/signals`.
|
|
464
|
+
|
|
465
|
+
```ts
|
|
466
|
+
import { batch, untracked } from "preact-sigma";
|
|
467
|
+
|
|
468
|
+
batch(() => {
|
|
469
|
+
counter.increment();
|
|
470
|
+
counter.reset();
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
untracked(() => {
|
|
474
|
+
console.log(counter.count);
|
|
475
|
+
});
|
|
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
|
+
|
|
482
|
+
```ts
|
|
483
|
+
import { defineManagedState, type StateHandle } from "preact-sigma";
|
|
484
|
+
|
|
485
|
+
type DialogState = boolean;
|
|
486
|
+
|
|
487
|
+
const Dialog = defineManagedState(
|
|
488
|
+
(dialog: StateHandle<DialogState>) => ({
|
|
489
|
+
open: dialog,
|
|
490
|
+
show() {
|
|
491
|
+
dialog.set(true);
|
|
492
|
+
},
|
|
493
|
+
hide() {
|
|
494
|
+
dialog.set(false);
|
|
495
|
+
},
|
|
496
|
+
}),
|
|
497
|
+
false,
|
|
498
|
+
);
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Keep using plain `useState()` when the state is trivial.
|
|
502
|
+
|
|
503
|
+
```tsx
|
|
504
|
+
const [open, setOpen] = useState(false);
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
## License
|
|
508
|
+
|
|
509
|
+
MIT. See [LICENSE-MIT](./LICENSE-MIT).
|