watch-state 3.5.1 → 3.6.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 CHANGED
@@ -30,7 +30,7 @@
30
30
  <br>
31
31
  <b>Light</b>
32
32
  <br>
33
- Less than 1 KB minzip
33
+ Less than 1 KB gzip
34
34
  </span></td>
35
35
  <td align="center"><span>
36
36
  <a href="https://d8corp.github.io/watch-state/coverage/lcov-report" target="_blank">
@@ -53,8 +53,14 @@
53
53
  <a href="https://www.npmtrends.com/watch-state" target="_blank">
54
54
  <img src="https://img.shields.io/npm/dm/watch-state.svg" alt="watch-state downloads">
55
55
  </a>
56
+ <a href="https://github.com/d8corp/watch-state/tree/master/release" target="_blank">
57
+ <img src="https://packagephobia.com/badge?p=watch-state" alt="watch-state install size">
58
+ </a>
59
+ <a href="https://cdn.jsdelivr.net/npm/watch-state" target="_blank">
60
+ <img src="https://img.badgesize.io/https:/cdn.jsdelivr.net/npm/watch-state?compression=gzip" alt="watch-state gzip size">
61
+ </a>
56
62
  <a href="https://www.typescriptlang.org" target="_blank">
57
- <img src="https://img.shields.io/npm/types/watch-state" alt="TypeSctipt">
63
+ <img src="https://img.shields.io/npm/types/watch-state" alt="TypeScript">
58
64
  </a>
59
65
  <a href="https://packagequality.com/#?package=watch-state" target="_blank">
60
66
  <img src="https://packagequality.com/shield/watch-state.svg" alt="watch-state quality">
@@ -62,7 +68,7 @@
62
68
  <a href="https://github.com/d8corp/watch-state/blob/master/LICENSE" target="_blank">
63
69
  <img src="https://img.shields.io/npm/l/watch-state" alt="watch-state license">
64
70
  </a>
65
- <a href="https://changelogs.xyz/watch-state" target="_blank">
71
+ <a href="https://github.com/d8corp/watch-state/blob/master/CHANGELOG.md" target="_blank">
66
72
  <img src="https://img.shields.io/badge/Changelog-⋮-brightgreen" alt="watch-state changelog">
67
73
  </a>
68
74
  <a href="https://d8corp.github.io/watch-state/coverage/lcov-report" target="_blank">
@@ -73,16 +79,26 @@
73
79
 
74
80
  `watch-state` is a **lightweight, high-performance reactive state engine** designed to power UI frameworks — **or replace them.**
75
81
 
76
- It provides **memory-safe reactivity** without Proxy, without magic, and without framework lock-in.
82
+ - **Fast** One of the fastest reactive libraries ([see benchmarks](#performance))
83
+ - **Light** — Less than 1 KB minzip
84
+ - **Zero-dependency** — No external packages required
85
+ - **Code splitting by design** — Decentralized state architecture, each page loads only the states it uses
86
+ - **Auto-subscription** — Dependencies tracked automatically, no manual subscriptions
87
+ - **Dynamic subscriptions** — Conditional watchers auto-subscribe/unsubscribe based on reactive conditions
88
+ - **Type-safe** — Full TypeScript support with type inference
89
+ - **Memory-safe** — Automatic cleanup on destroy
90
+ - **Lazy computation** — Compute executes only when accessed
91
+ - **No Proxy** — Supports old browsers (Firefox 45+, Safari 9+)
92
+ - **Framework-agnostic** — Business logic lives outside components, reusable across any framework or vanilla JS
77
93
 
78
94
  Use it as the core state layer in your own framework, embed it in React components, or build a full UI — **no JSX, no virtual DOM, no framework required**.
79
95
 
80
- Was born during working on [@innet/dom](https://www.npmjs.com/package/@innet/dom).
96
+ Born while working on [@innet/dom](https://www.npmjs.com/package/@innet/dom).
81
97
 
82
98
  [![stars](https://img.shields.io/github/stars/d8corp/watch-state?style=social)](https://github.com/d8corp/watch-state/stargazers)
83
99
  [![watchers](https://img.shields.io/github/watchers/d8corp/watch-state?style=social)](https://github.com/d8corp/watch-state/watchers)
84
100
 
85
- ## Browser supports
101
+ ## Browser Support
86
102
 
87
103
  ### Desktop
88
104
 
@@ -96,17 +112,17 @@ Was born during working on [@innet/dom](https://www.npmjs.com/package/@innet/dom
96
112
  |:-------:|:------:|:------:|:-----:|
97
113
  | 87+ | 90+ | 9+ | 62+ |
98
114
 
99
- *You can transpile it supporting old browsers, but the performance decreases.*
115
+ *You can transpile the code to support browsers older than listed above, but performance will decrease.*
100
116
 
101
117
  ## Index
102
118
 
103
119
  <sup>**[ [Install](#install) ]**</sup>
104
- <sup>**[ [Usage](#usage) ]** [Simple example](#simple-example) • [Example Vanilla JS](#example-vanilla-js) • [Example React](#example-react) • [Example @innet/dom](#example-innetdom)</sup>
105
- <sup>**[ [Watch](#watch) ]** [Update argument](#update-argument) • [Force update of Watch](#force-update-of-watch) • [Destroy Watch](#destroy-watch) • [Deep/Nested watchers](#deepnested-watchers)</sup>
106
- <sup>**[ [State](#state) ]** [Get or Set value](#get-or-set-value) • [Force update of State](#force-update-of-state) • [Raw value](#raw-value)</sup>
120
+ <sup>**[ [Usage](#usage) ]** [Simple example](#simple-example) • [Example Vanilla JS](#example-vanilla-js) • [Example React](#example-react) • [Example Innet](#example-innet)</sup>
121
+ <sup>**[ [Watch](#watch) ]** [Force update of Watch](#force-update-of-watch) • [Destroy Watch](#destroy-watch) • [Deep/Nested Watchers](#deepnested-watchers)</sup>
122
+ <sup>**[ [State](#state) ]** [Get or Set value](#get-or-set-value) • [State.set](#stateset) • [Force update of State](#force-update-of-state) • [Raw value](#raw-value) • [Initial value](#initial-value) • [Reset value](#reset-value)</sup>
107
123
  <sup>**[ [Compute](#compute) ]** [Lazy computation](#lazy-computation) • [Force update of Compute](#force-update-of-compute) • [Destroy Compute](#destroy-compute)</sup>
108
124
  <sup>**[ [Utils](#utils) ]** [onDestroy](#ondestroy) • [callEvent](#callevent) • [createEvent](#createevent) • [unwatch](#unwatch)</sup>
109
- <sup>**[ [Typescript](#typescript) ]**</sup>
125
+ <sup>**[ [Typescript](#typescript) ]** [State type inference](#state-type-inference) • [Compute type inference](#compute-type-inference)</sup>
110
126
  <sup>**[ [Performance](#performance) ]**</sup>
111
127
 
112
128
  ## Install
@@ -132,7 +148,7 @@ html
132
148
  ## Usage
133
149
  ###### [🏠︎](#index) / Usage [↑](#install) [↓](#watch)
134
150
 
135
- <sup>[Simple example](#simple-example) • [Example Vanilla JS](#example-vanilla-js) • [Example React](#example-react) • [Example @innet/dom](#example-innetdom)</sup>
151
+ <sup>[Simple example](#simple-example) • [Example Vanilla JS](#example-vanilla-js) • [Example React](#example-react) • [Example Innet](#example-innet)</sup>
136
152
 
137
153
  The library is based on the core concepts of `Observable` (something that can be observed) and `Observer` (something that can observe). On top of these concepts, the core classes `State`, `Compute`, and `Watch` are built according to the following scheme:
138
154
 
@@ -158,13 +174,13 @@ import { Watch, State } from 'watch-state'
158
174
  const count = new State(0)
159
175
 
160
176
  new Watch(() => console.log(count.value))
161
- // console.log(0)
177
+ // logs: 0
162
178
 
163
179
  count.value++
164
- // console.log(1)
180
+ // logs: 1
165
181
 
166
182
  count.value++
167
- // console.log(2)
183
+ // logs: 2
168
184
  ```
169
185
 
170
186
  ### Example Vanilla JS
@@ -202,9 +218,9 @@ Simple reactive state without build tools or framework dependencies.
202
218
  ```
203
219
 
204
220
  ### Example React
205
- ###### [🏠︎](#index) / [Usage](#usage) / Example React [↑](#example-vanilla-js) [↓](#example-innetdom)
221
+ ###### [🏠︎](#index) / [Usage](#usage) / Example React [↑](#example-vanilla-js) [↓](#example-innet)
206
222
 
207
- [@watch-state/react](https://www.npmjs.com/package/@watch-state/react) provides `useObservable()` hook that automatically subscribes React components to state changes and re-renders only when needed.
223
+ [@watch-state/react](https://www.npmjs.com/package/@watch-state/react) provides hooks that automatically subscribe React components to state changes and re-renders only when needed.
208
224
 
209
225
  ```tsx
210
226
  import { State } from 'watch-state'
@@ -223,12 +239,10 @@ export function CountButton () {
223
239
  }
224
240
  ```
225
241
 
226
- ### Example @innet/dom
227
- ###### [🏠︎](#index) / [Usage](#usage) / Example @innet/dom [↑](#example-react)
242
+ ### Example Innet
243
+ ###### [🏠︎](#index) / [Usage](#usage) / Example Innet [↑](#example-react)
228
244
 
229
- **Zero-runtime reactivity with [@innet/dom](https://www.npmjs.com/package/@innet/dom):**
230
-
231
- `@innet/dom` automatically watches accessed states and **updates only changed DOM content** — **no full re-renders**.
245
+ [@innet/dom](https://www.npmjs.com/package/@innet/dom) automatically watches accessed states and **updates only changed DOM content** — **no full re-renders**.
232
246
 
233
247
  ```tsx
234
248
  import { State } from 'watch-state'
@@ -244,87 +258,33 @@ export function CountButton () {
244
258
  }
245
259
  ```
246
260
 
247
- Key benefits:
248
-
249
- - No Watch or useWatch needed — framework handles reactivity
250
- - Only button content updates, no re-renders of component/DOM tree
251
- - Direct state access {count} auto-triggers minimal updates
252
- - Works with any JSX/TSX without extra setup
253
-
254
261
  ## Watch
255
262
  ###### [🏠︎](#index) / Watch [↑](#usage) [↓](#state)
256
263
 
257
- <sup>[Update argument](#update-argument) • [Force update of Watch](#force-update-of-watch) • [Destroy Watch](#destroy-watch) • [Deep/Nested watchers](#deepnested-watchers)</sup>
258
-
259
- **Reactive effect that automatically tracks and reacts to state changes.**
264
+ <sup>[Force update of Watch](#force-update-of-watch) • [Destroy Watch](#destroy-watch) • [Deep/Nested watchers](#deepnested-watchers)</sup>
260
265
 
261
- `Watch` executes a callback when any accessed `State.value` changes.
262
- Accessing `.value` **inside the callback auto-subscribes** to that state — no manual registration needed.
266
+ `Watch` accepts a **reaction** as its first argument and executes it when any accessed state changes.
267
+ State accessed inside a reaction is **auto-subscribed** — no manual registration needed.
263
268
 
264
269
  ```ts
265
- // Create state
266
- const count = new State(0)
267
-
268
- // Create watcher that logs the state changes
269
- new Watch(() => console.log(count.value)) // auto-subscribes to count
270
-
271
- count.value = 1 // triggers watcher callback
272
- ```
273
-
274
- ### Update argument
275
- ###### [🏠︎](#index) / [Watch](#watch) / Update argument [↓](#force-update-of-watch)
276
-
277
- **Distinguish initial run from updates using `update` parameter.**
278
-
279
- `update` is `false` on **first execution** (initial subscription), `true` on **subsequent re-runs** when states change.
280
-
281
- ```javascript
282
- const count = new State(0)
283
-
284
- new Watch(update => {
285
- console.log(update, count.value)
286
- })
287
- // console.log(false, 0)
288
-
289
- count.value++
290
- // console.log(true, 1)
291
-
292
- count.value++
293
- // console.log(true, 2)
294
- ```
295
-
296
- **Watch state once using `update` flag and auto-destroy:**
297
-
298
- ```typescript jsx
299
- const count = new State(0)
300
-
301
- new Watch(update => {
302
-
303
- if (!update) {
304
-
305
- // Watch this value
306
- count.value
307
-
308
- } else {
309
-
310
- // React on changes
311
- console.log('The value was changed')
312
-
313
- }
270
+ const state = new State(0)
314
271
 
315
- })
272
+ const reaction = () => {
273
+ console.log(state.value)
274
+ // auto-subscribes to state
275
+ }
316
276
 
317
- count.value++
318
- // console.log('The value was changed')
277
+ new Watch(reaction)
278
+ // logs: 0
319
279
 
320
- count.value++
321
- // nothing happens
280
+ state.value = 1 // triggers reaction
281
+ // logs: 1
322
282
  ```
323
283
 
324
284
  ### Force update of Watch
325
- ###### [🏠︎](#index) / [Watch](#watch) / Force update of Watch [↑](#update-argument) [↓](#destroy-watch)
285
+ ###### [🏠︎](#index) / [Watch](#watch) / Force update of Watch [↓](#destroy-watch)
326
286
 
327
- You can run a watcher even when it's states are not updated.
287
+ You can run a reaction even when its states are not updated.
328
288
 
329
289
  ```typescript
330
290
  const count = new State(0)
@@ -332,10 +292,10 @@ const count = new State(0)
332
292
  const watcher = new Watch(() => {
333
293
  console.log(count.value)
334
294
  })
335
- // console.log(0)
295
+ // logs: 0
336
296
 
337
297
  watcher.update()
338
- // console.log(0)
298
+ // logs: 0
339
299
  ```
340
300
 
341
301
  ### Destroy Watch
@@ -349,10 +309,10 @@ const count = new State(0)
349
309
  const watcher = new Watch(() => {
350
310
  console.log(count.value)
351
311
  })
352
- // console.log(0)
312
+ // logs: 0
353
313
 
354
314
  count.value++
355
- // console.log(1)
315
+ // logs: 1
356
316
 
357
317
  watcher.destroy()
358
318
 
@@ -360,12 +320,10 @@ count.value++
360
320
  // nothing happens
361
321
  ```
362
322
 
363
- ### Deep/Nested watchers
364
- ###### [🏠︎](#index) / [Watch](#watch) / Deep/Nested watchers [↑](#destroy-watch)
323
+ ### Deep/Nested Watchers
324
+ ###### [🏠︎](#index) / [Watch](#watch) / Deep/Nested Watchers [↑](#destroy-watch)
365
325
 
366
- **Create conditional and nested reactive effects.**
367
-
368
- Each `Watch` **independently tracks only states accessed within its callback**.
326
+ Each `Watch` **independently tracks only states accessed within its reaction**.
369
327
  Nested watchers created inside parent watchers form a **dependency tree** with separate reactivity.
370
328
 
371
329
  ```javascript
@@ -396,14 +354,14 @@ state.value++
396
354
  ## State
397
355
  ###### [🏠︎](#index) / State [↑](#watch) [↓](#compute)
398
356
 
399
- <sup>[Get or Set value](#get-or-set-value) • [Force update of State](#force-update-of-state) • [Raw value](#raw-value)</sup>
357
+ <sup>[Get or Set value](#get-or-set-value) • [State.set](#stateset) • [Force update of State](#force-update-of-state) • [Raw value](#raw-value) • [Initial value](#initial-value) • [Reset value](#reset-value)</sup>
400
358
 
401
- **Reactive primitive** that automatically notifies all subscribed watchers when `.value` changes.
359
+ **Reactive primitive** that holds a value and automatically notifies all subscribers when it changes.
402
360
 
403
361
  ### Get or Set value
404
- ###### [🏠︎](#index) / [State](#state) / Get or Set value [↓](#force-update-of-state)
362
+ ###### [🏠︎](#index) / [State](#state) / Get or Set value [↓](#stateset)
405
363
 
406
- **Access or mutate the state value.** Reading `.value` inside `Watch` **auto-subscribes** to changes. Writing `.value` **triggers all watchers**.
364
+ Reading `.value` inside reaction **auto-subscribes** to changes. Writing `.value` **triggers all reactions**.
407
365
 
408
366
  ```ts
409
367
  const count = new State(0)
@@ -414,34 +372,55 @@ new Watch(() => console.log(count.value))
414
372
  count.value++ // triggers: logs 1
415
373
  ```
416
374
 
375
+ ### State.set
376
+ ###### [🏠︎](#index) / [State](#state) / State.set [↑](#get-or-set-value) [↓](#force-update-of-state)
377
+
378
+ `State.set` mirrors the behavior of the value setter but returns `void`.
379
+ It is useful as a shorthand in arrow functions: `() => state.set(nextValue)` instead of `() => { state.value = nextValue }`.
380
+
381
+ Note: `state.set` cannot be used as a standalone function; `const set = state.set` is not supported.
382
+
383
+ ```ts
384
+ const count = new State(0)
385
+
386
+ // Subscribing
387
+ new Watch(() => console.log(count.value))
388
+ // logs: 0
389
+
390
+ count.set(1)
391
+ // logs: 1
392
+ ```
393
+
417
394
  ### Force update of State
418
- ###### [🏠︎](#index) / [State](#state) / Force update of State [↑](#get-or-set-value) [↓](#raw-value)
395
+ ###### [🏠︎](#index) / [State](#state) / Force update of State [↑](#stateset) [↓](#raw-value)
419
396
 
420
- You can run watchers of a state with `update` method.
397
+ You can run reactions of a state with `update` method.
421
398
 
422
399
  ```ts
423
400
  // Create state
424
- const log = new State([])
401
+ const log = new State<number[]>([])
425
402
 
426
403
  // Subscribe to changes
427
404
  new Watch(() => console.log(log.value)) // logs: []
428
405
 
406
+ // Modify the array
429
407
  log.value.push(1) // no logs
408
+ log.value.push(2) // no logs
430
409
 
431
410
  // Update value
432
- log.update() // logs: [1]
411
+ log.update() // logs: [1, 2]
433
412
  ```
434
413
 
435
414
  ### Raw value
436
- ###### [🏠︎](#index) / [State](#state) / Raw value [↑](#force-update-of-state)
415
+ ###### [🏠︎](#index) / [State](#state) / Raw value [↑](#force-update-of-state) [↓](#initial-value)
437
416
 
438
- `rawValue` returns the current value but **doesn't subscribe** to changes — unlike `value` which auto-subscribes in `Watch`.
417
+ `raw` returns the current value but does not subscribe to changes — unlike `value`.
439
418
 
440
419
  ```ts
441
420
  const foo = new State(0)
442
421
  const bar = new State(0)
443
422
 
444
- new Watch(() => console.log(foo.value, bar.rawValue))
423
+ new Watch(() => console.log(foo.value, bar.raw))
445
424
  // logs: 0, 0
446
425
 
447
426
  foo.value++ // logs: 1, 0
@@ -449,19 +428,62 @@ bar.value++ // no logs
449
428
  foo.value++ // logs: 2, 1
450
429
  ```
451
430
 
431
+ ### Initial value
432
+ ###### [🏠︎](#index) / [State](#state) / Initial value [↑](#raw-value) [↓](#reset-value)
433
+
434
+ `initial` stores the initial value passed to the constructor.
435
+ Useful for checking if the state has been modified by comparing `state.initial === state.raw`.
436
+
437
+ ```ts
438
+ const count = new State(0)
439
+
440
+ console.log(count.initial)
441
+ // logs: 0
442
+
443
+ count.value = 5
444
+ console.log(count.initial === count.raw)
445
+ // logs: false
446
+
447
+ count.reset()
448
+ console.log(count.initial === count.raw)
449
+ // logs: true
450
+ ```
451
+
452
+ ### Reset value
453
+ ###### [🏠︎](#index) / [State](#state) / Reset value [↑](#initial-value)
454
+
455
+ `reset()` restores the state to its initial value.
456
+ Triggers watchers only if the current value differs from the initial value.
457
+
458
+ ```ts
459
+ const count = new State(0)
460
+
461
+ new Watch(() => console.log(count.value))
462
+ // logs: 0
463
+
464
+ count.value = 5
465
+ // logs: 5
466
+
467
+ count.reset()
468
+ // logs: 0
469
+
470
+ count.reset()
471
+ // no logs (value already 0)
472
+ ```
473
+
452
474
  ## Compute
453
475
  ###### [🏠︎](#index) / Compute [↑](#state) [↓](#utils)
454
476
 
455
477
  <sup>[Lazy computation](#lazy-computation) • [Force update of Compute](#force-update-of-compute) • [Destroy Compute](#destroy-compute)</sup>
456
478
 
457
- **Derived reactive state** that automatically recomputes when its dependencies change.
458
- **Lazy execution** only computes when `.value` is accessed.
479
+ `Compute` accepts a **reaction** as its first argument and represents a reactive value returned by the reaction.
480
+ It creates a **derived state** that automatically tracks dependencies and caches the result.
459
481
 
460
482
  ### Lazy computation
461
483
  ###### [🏠︎](#index) / [Compute](#compute) / Lazy computation [↓](#force-update-of-compute)
462
484
 
463
485
  `Compute` doesn't execute immediately — waits for `.value` access.
464
- Dependencies (`State.value` reads inside callback) auto-subscribe like `Watch`.
486
+ Dependencies (`State.value` reads inside reaction) auto-subscribe like `Watch`.
465
487
 
466
488
  ```javascript
467
489
  const name = new State('Foo')
@@ -484,60 +506,41 @@ surname.value = 'Quux' // surname[0] = "Q"
484
506
  // logs: 'Foo Q'
485
507
  ```
486
508
 
487
- **Benefits:**
488
- - **Zero overhead** for unused computed values
489
- - **Automatic dependency tracking** — no manual subscriptions
490
- - **Cached result** — same `.value` reads return cached value
491
-
492
509
  ### Force update of Compute
493
510
  ###### [🏠︎](#index) / [Compute](#compute) / Force update of Compute [↑](#lazy-computation) [↓](#destroy-compute)
494
511
 
495
- **Call `.update()` to manually trigger recomputation** forces callback execution **even when no dependencies changed**.
496
-
497
- **Perfect for:**
498
- - **Array mutations** (`push`, `pop`, `splice`)
499
- - **Object mutations** (adding properties)
500
- - **External data refresh**
501
- - **Debugging** stale values
512
+ You can run a reaction of a compute with `update` method.
502
513
 
503
514
  ```ts
504
515
  const items = new State([])
505
516
 
506
517
  const itemCount = new Compute(() => {
507
- console.log('🔄 Recomputing length...')
518
+ console.log('Recomputing length...')
508
519
  return items.value.length
509
520
  })
510
521
 
511
522
  new Watch(() => console.log('Watcher sees:', itemCount.value))
512
- // 🔄 Recomputing length...
513
- // Watcher sees: 0
523
+ // logs: Recomputing length...
524
+ // logs: Watcher sees: 0
514
525
 
515
- items.value.push('apple') // ❌ Array reference SAME → NO recompute!
516
- console.log('Direct length:', items.value.length) // 1
517
- console.log(itemCount.value) // STALE: 0 ❌
526
+ items.value.push('apple')
527
+ // Array reference SAME → NO recompute!
518
528
 
519
- itemCount.update() // ✅ FORCES recompute
520
- // 🔄 Recomputing length...
521
- // Watcher sees: 1
529
+ itemCount.update()
530
+ // logs: Recomputing length...
531
+ // logs: Watcher sees: 1
522
532
  ```
523
533
 
524
534
  ### Destroy Compute
525
535
  ###### [🏠︎](#index) / [Compute](#compute) / Destroy Compute [↑](#force-update-of-compute)
526
536
 
527
- Call `.destroy()` to completely stop reactivity unsubscribes from all dependency states, clears cached value, and prevents any future recomputations.
528
-
529
- Triggers `onDestroy` callbacks registered inside `Compute` callback:
537
+ You can stop watching by `destroy` method of `Compute`.
530
538
 
531
539
  ```ts
532
540
  const user = new State({ name: 'Alice', age: 30 })
533
541
 
534
542
  const userName = new Compute(() => {
535
543
  console.log('Computing')
536
-
537
- onDestroy(() => {
538
- console.log('Cleanup')
539
- })
540
-
541
544
  return user.value.name.toUpperCase()
542
545
  })
543
546
 
@@ -546,12 +549,10 @@ new Watch(() => console.log(userName.value))
546
549
  // logs: ALICE
547
550
 
548
551
  user.value = { name: 'Mike', age: 32 }
549
- // logs: Cleanup
550
552
  // logs: Computing
551
553
  // logs: MIKE
552
554
 
553
555
  userName.destroy()
554
- // logs: Cleanup
555
556
 
556
557
  user.value = { name: 'Bob', age: 31 }
557
558
  // nothing happens — fully disconnected!
@@ -591,23 +592,16 @@ count.value++
591
592
  ### callEvent
592
593
  ###### [🏠︎](#index) / [Utils](#utils) / callEvent [↑](#ondestroy) [↓](#createevent)
593
594
 
594
- **Immediately executes reactive effect** (unlike [createEvent](#createevent)).
595
+ You can immediately execute a reactive effect with `callEvent`.
595
596
 
596
- Both `callEvent` and `createEvent`:
597
- - **Ignores** automatic state subscriptions (`unwatch`)
598
- - **Batches** state updates and **flushes queue** at the end
599
- - Perfect for **side effects** and **mutations**
600
-
601
- **Key differences:**
602
- - `callEvent(fn)` → **executes NOW** and returns result
603
- - `createEvent(fn)` → **returns reusable function**
597
+ `callEvent` batches all state updates inside the callback and triggers watchers only once at the end.
604
598
 
605
599
  ```ts
606
600
  const a = new State(0)
607
601
  const b = new State(0)
608
602
 
609
603
  new Watch(() => {
610
- console.log(a.value, b.value)
604
+ console.log(a.value, b.value)
611
605
  })
612
606
  // logs: 0, 0
613
607
 
@@ -618,14 +612,13 @@ b.value = 1
618
612
  // logs: 1, 1
619
613
 
620
614
  callEvent(() => {
621
- a.value = 2
622
- b.value = 2
615
+ a.value = 2
616
+ b.value = 2
623
617
  })
624
- // logs: 1, 1
618
+ // logs: 2, 2
625
619
  ```
626
620
 
627
- `callEvent` executes your callback and returns exactly what your callback
628
- returns — TypeScript infers the correct type automatically.
621
+ `callEvent` returns exactly what your callback returns TypeScript infers the correct type automatically.
629
622
 
630
623
  ```ts
631
624
  const count = new State(0)
@@ -643,31 +636,30 @@ console.log(prev)
643
636
  ### createEvent
644
637
  ###### [🏠︎](#index) / [Utils](#utils) / createEvent [↑](#callevent) [↓](#unwatch)
645
638
 
646
- You can create event function with `createEvent`
639
+ You can create a reusable event function with `createEvent`.
640
+
641
+ Like `callEvent`, it batches state updates and triggers watchers only once after execution.
642
+
647
643
  ```typescript
648
644
  import { State, createEvent } from 'watch-state'
649
645
 
650
646
  const count = new State(0)
651
- const increase = createEvent(() => {
652
- console.log(count.value++)
653
- })
647
+ const increase = createEvent(() => count.value++)
654
648
 
655
649
  new Watch(() => console.log(count.value))
656
- // console.log(0)
650
+ // logs: 0
657
651
 
658
652
  increase()
659
- // console.log(1)
653
+ // logs: 1
660
654
 
661
655
  increase()
662
- // console.log(2)
656
+ // logs: 2
663
657
  ```
664
658
 
665
659
  ### unwatch
666
660
  ###### [🏠︎](#index) / [Utils](#utils) / unwatch [↑](#createevent)
667
661
 
668
- **Disables automatic state subscriptions** by wrapping value access in `unwatch`.
669
-
670
- **Unlike `callEvent`/`createEvent`**, `unwatch` does **NOT batch updates**.
662
+ You can disable automatic state subscriptions with `unwatch`.
671
663
 
672
664
  ```ts
673
665
  import { State, Watch, unwatch } from 'watch-state'
@@ -676,29 +668,86 @@ const count = new State(0)
676
668
 
677
669
  new Watch(() => {
678
670
  console.log(unwatch(() => count.value++))
679
- }) // logs: 0
671
+ })
672
+ // logs: 0
680
673
 
681
- count.value++ // logs: 1
674
+ count.value++
675
+ // logs: 1
682
676
 
683
- console.log(count.value) // logs: 2
677
+ console.log(count.value)
678
+ // logs: 2
684
679
  ```
685
680
 
686
681
  ## Typescript
687
682
  ###### [🏠︎](#index) / Typescript [↑](#utils) [↓](#performance)
688
683
 
689
- Generic of `State`
684
+ ### State type inference
685
+ ###### [🏠︎](#index) / [Typescript](#typescript) / State type inference [↓](#compute-type-inference)
686
+
687
+ **Type inference from initial value:**
688
+ Type is automatically inferred from the initial value passed to the constructor — no generic needed.
690
689
  ```typescript
691
- const key = new State<string | number>()
690
+ const count = new State(0) // State<number>
692
691
 
693
- key.value = false
694
- // error, you can use only string or number
692
+ count.value = 'str' // error: number expected
695
693
  ```
696
- Generic of `Compute`
694
+
695
+ **Without initial value:**
696
+ When using a generic without an initial value, `initial` is `undefined`, which may conflict with strict types.
697
+
697
698
  ```typescript
698
- new Compute<string>(() => false)
699
- // error, target of `Compute` should return string
699
+ const value = new State<string>()
700
+ // value.initial is undefined (not string)
701
+
702
+ // To allow undefined in type:
703
+ const maybe = new State<string | undefined>()
700
704
  ```
701
705
 
706
+ **State as a type annotation:**
707
+ Without a generic, `State` defaults to `State<unknown>`, which accepts any value type.
708
+
709
+ ```typescript
710
+ const foo: State = new State(0)
711
+
712
+ foo.value = 'str' // ok (unknown allows any)
713
+ foo.value = true // ok
714
+
715
+ // Specify generic for type safety:
716
+ const bar: State<number> = new State(0)
717
+
718
+ bar.value = 'str' // error
719
+ ```
720
+
721
+ ### Compute type inference
722
+ ###### [🏠︎](#index) / [Typescript](#typescript) / Compute type inference [↑](#state-type-inference)
723
+
724
+ **Type inferred from function return:**
725
+ Type is automatically inferred from the function's return value — no generic needed.
726
+ ```typescript
727
+ const fullName = new Compute(() => `${firstName.value} ${lastName.value}`)
728
+ // Compute<string> — no generic needed
729
+
730
+ const length = new Compute(() => items.value.length)
731
+ // Compute<number>
732
+ ```
733
+
734
+ **Explicit generic (usually not needed):**
735
+ Explicit generics are rarely needed since types are inferred. Use only when you want to enforce a specific type.
736
+ ```typescript
737
+ new Compute<string>(() => false) // error: boolean not assignable to string
738
+ ```
739
+
740
+ **Destroyed Compute and undefined:**
741
+ `Compute.value` is typed as the function return type, but if you access `.value` after `destroy()` (before any computation ran), it returns `undefined`.
742
+ ```typescript
743
+ const computed = new Compute(() => expensiveCalculation())
744
+
745
+ computed.destroy()
746
+ console.log(computed.value) // undefined (but typed as return type)
747
+ ```
748
+
749
+ This is intentional — accessing destroyed observers is rare and shouldn't require `undefined` checks in normal code.
750
+
702
751
  ## Performance
703
752
  ###### [🏠︎](#index) / Performance [↑](#typescript)
704
753