synstate 0.1.1 → 1.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 +317 -298
- package/dist/core/class/child-observable-class.d.mts.map +1 -1
- package/dist/core/class/child-observable-class.mjs +43 -10
- package/dist/core/class/child-observable-class.mjs.map +1 -1
- package/dist/core/class/observable-base-class.d.mts +4 -4
- package/dist/core/class/observable-base-class.d.mts.map +1 -1
- package/dist/core/class/observable-base-class.mjs +8 -8
- package/dist/core/class/observable-base-class.mjs.map +1 -1
- package/dist/core/class/root-observable-class.d.mts +1 -1
- package/dist/core/class/root-observable-class.d.mts.map +1 -1
- package/dist/core/class/root-observable-class.mjs +9 -9
- package/dist/core/class/root-observable-class.mjs.map +1 -1
- package/dist/core/combine/combine.d.mts +7 -7
- package/dist/core/combine/combine.mjs +13 -14
- package/dist/core/combine/combine.mjs.map +1 -1
- package/dist/core/combine/merge.d.mts +6 -6
- package/dist/core/combine/merge.mjs +9 -9
- package/dist/core/combine/merge.mjs.map +1 -1
- package/dist/core/combine/zip.d.mts +20 -19
- package/dist/core/combine/zip.d.mts.map +1 -1
- package/dist/core/combine/zip.mjs +22 -21
- package/dist/core/combine/zip.mjs.map +1 -1
- package/dist/core/create/{interval.d.mts → counter.d.mts} +14 -12
- package/dist/core/create/counter.d.mts.map +1 -0
- package/dist/core/create/{interval.mjs → counter.mjs} +21 -23
- package/dist/core/create/counter.mjs.map +1 -0
- package/dist/core/create/from-abortable-promise.d.mts +29 -0
- package/dist/core/create/from-abortable-promise.d.mts.map +1 -0
- package/dist/core/create/from-abortable-promise.mjs +70 -0
- package/dist/core/create/from-abortable-promise.mjs.map +1 -0
- package/dist/core/create/from-promise.d.mts +9 -6
- package/dist/core/create/from-promise.d.mts.map +1 -1
- package/dist/core/create/from-promise.mjs +8 -5
- package/dist/core/create/from-promise.mjs.map +1 -1
- package/dist/core/create/from-subscribable.d.mts +4 -4
- package/dist/core/create/from-subscribable.mjs +4 -4
- package/dist/core/create/index.d.mts +3 -3
- package/dist/core/create/index.d.mts.map +1 -1
- package/dist/core/create/index.mjs +4 -4
- package/dist/core/create/just.d.mts +32 -0
- package/dist/core/create/just.d.mts.map +1 -0
- package/dist/core/create/just.mjs +44 -0
- package/dist/core/create/just.mjs.map +1 -0
- package/dist/core/create/source.d.mts +7 -12
- package/dist/core/create/source.d.mts.map +1 -1
- package/dist/core/create/source.mjs +1 -6
- package/dist/core/create/source.mjs.map +1 -1
- package/dist/core/create/timer.d.mts +6 -4
- package/dist/core/create/timer.d.mts.map +1 -1
- package/dist/core/create/timer.mjs +6 -7
- package/dist/core/create/timer.mjs.map +1 -1
- package/dist/core/index.d.mts +1 -1
- package/dist/core/index.d.mts.map +1 -1
- package/dist/core/index.mjs +21 -14
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/operators/audit.d.mts +97 -0
- package/dist/core/operators/audit.d.mts.map +1 -0
- package/dist/core/operators/audit.mjs +144 -0
- package/dist/core/operators/audit.mjs.map +1 -0
- package/dist/core/operators/debounce.d.mts +88 -0
- package/dist/core/operators/debounce.d.mts.map +1 -0
- package/dist/core/operators/debounce.mjs +130 -0
- package/dist/core/operators/debounce.mjs.map +1 -0
- package/dist/core/operators/filter.d.mts +5 -5
- package/dist/core/operators/filter.mjs +3 -3
- package/dist/core/operators/filter.mjs.map +1 -1
- package/dist/core/operators/index.d.mts +4 -4
- package/dist/core/operators/index.d.mts.map +1 -1
- package/dist/core/operators/index.mjs +6 -6
- package/dist/core/operators/map.d.mts +41 -0
- package/dist/core/operators/map.d.mts.map +1 -0
- package/dist/core/operators/map.mjs +71 -0
- package/dist/core/operators/map.mjs.map +1 -0
- package/dist/core/operators/merge-map.d.mts +57 -30
- package/dist/core/operators/merge-map.d.mts.map +1 -1
- package/dist/core/operators/merge-map.mjs +59 -32
- package/dist/core/operators/merge-map.mjs.map +1 -1
- package/dist/core/operators/pairwise.d.mts +6 -6
- package/dist/core/operators/pairwise.mjs +9 -9
- package/dist/core/operators/pairwise.mjs.map +1 -1
- package/dist/core/operators/scan.d.mts +6 -6
- package/dist/core/operators/scan.mjs +9 -9
- package/dist/core/operators/scan.mjs.map +1 -1
- package/dist/core/operators/skip-if-no-change.d.mts +21 -9
- package/dist/core/operators/skip-if-no-change.d.mts.map +1 -1
- package/dist/core/operators/skip-if-no-change.mjs +25 -13
- package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
- package/dist/core/operators/skip-until.d.mts +5 -5
- package/dist/core/operators/skip-until.mjs +8 -8
- package/dist/core/operators/skip-until.mjs.map +1 -1
- package/dist/core/operators/skip-while.d.mts +18 -9
- package/dist/core/operators/skip-while.d.mts.map +1 -1
- package/dist/core/operators/skip-while.mjs +28 -16
- package/dist/core/operators/skip-while.mjs.map +1 -1
- package/dist/core/operators/switch-map.d.mts +57 -26
- package/dist/core/operators/switch-map.d.mts.map +1 -1
- package/dist/core/operators/switch-map.mjs +59 -28
- package/dist/core/operators/switch-map.mjs.map +1 -1
- package/dist/core/operators/take-until.d.mts +5 -5
- package/dist/core/operators/take-until.mjs +8 -8
- package/dist/core/operators/take-until.mjs.map +1 -1
- package/dist/core/operators/take-while.d.mts +15 -8
- package/dist/core/operators/take-while.d.mts.map +1 -1
- package/dist/core/operators/take-while.mjs +19 -13
- package/dist/core/operators/take-while.mjs.map +1 -1
- package/dist/core/operators/throttle.d.mts +81 -0
- package/dist/core/operators/throttle.d.mts.map +1 -0
- package/dist/core/operators/throttle.mjs +126 -0
- package/dist/core/operators/throttle.mjs.map +1 -0
- package/dist/core/operators/with-buffered-from.d.mts +13 -9
- package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
- package/dist/core/operators/with-buffered-from.mjs +17 -13
- package/dist/core/operators/with-buffered-from.mjs.map +1 -1
- package/dist/core/operators/with-current-value-from.d.mts +14 -9
- package/dist/core/operators/with-current-value-from.d.mts.map +1 -1
- package/dist/core/operators/with-current-value-from.mjs +18 -13
- package/dist/core/operators/with-current-value-from.mjs.map +1 -1
- package/dist/core/operators/with-initial-value.d.mts +5 -5
- package/dist/core/operators/with-initial-value.mjs +8 -8
- package/dist/core/operators/with-initial-value.mjs.map +1 -1
- package/dist/core/predefined/index.d.mts +2 -0
- package/dist/core/predefined/index.d.mts.map +1 -0
- package/dist/core/predefined/index.mjs +12 -0
- package/dist/core/predefined/index.mjs.map +1 -0
- package/dist/core/predefined/operators/attach-index.d.mts +57 -0
- package/dist/core/predefined/operators/attach-index.d.mts.map +1 -0
- package/dist/core/predefined/operators/attach-index.mjs +62 -0
- package/dist/core/predefined/operators/attach-index.mjs.map +1 -0
- package/dist/core/predefined/operators/index.d.mts +12 -0
- package/dist/core/predefined/operators/index.d.mts.map +1 -0
- package/dist/core/predefined/operators/index.mjs +12 -0
- package/dist/core/predefined/operators/index.mjs.map +1 -0
- package/dist/core/predefined/operators/map-optional.d.mts +51 -0
- package/dist/core/predefined/operators/map-optional.d.mts.map +1 -0
- package/dist/core/predefined/operators/map-optional.mjs +55 -0
- package/dist/core/predefined/operators/map-optional.mjs.map +1 -0
- package/dist/core/predefined/operators/map-result-err.d.mts +51 -0
- package/dist/core/predefined/operators/map-result-err.d.mts.map +1 -0
- package/dist/core/predefined/operators/map-result-err.mjs +55 -0
- package/dist/core/predefined/operators/map-result-err.mjs.map +1 -0
- package/dist/core/predefined/operators/map-result-ok.d.mts +51 -0
- package/dist/core/predefined/operators/map-result-ok.d.mts.map +1 -0
- package/dist/core/predefined/operators/map-result-ok.mjs +55 -0
- package/dist/core/predefined/operators/map-result-ok.mjs.map +1 -0
- package/dist/core/predefined/operators/map-to.d.mts +43 -0
- package/dist/core/predefined/operators/map-to.d.mts.map +1 -0
- package/dist/core/predefined/operators/map-to.mjs +48 -0
- package/dist/core/predefined/operators/map-to.mjs.map +1 -0
- package/dist/core/predefined/operators/pluck.d.mts +47 -0
- package/dist/core/predefined/operators/pluck.d.mts.map +1 -0
- package/dist/core/predefined/operators/pluck.mjs +52 -0
- package/dist/core/predefined/operators/pluck.mjs.map +1 -0
- package/dist/core/predefined/operators/skip.d.mts +50 -0
- package/dist/core/predefined/operators/skip.d.mts.map +1 -0
- package/dist/core/predefined/operators/skip.mjs +56 -0
- package/dist/core/predefined/operators/skip.mjs.map +1 -0
- package/dist/core/predefined/operators/take.d.mts +44 -0
- package/dist/core/predefined/operators/take.d.mts.map +1 -0
- package/dist/core/predefined/operators/take.mjs +49 -0
- package/dist/core/predefined/operators/take.mjs.map +1 -0
- package/dist/core/predefined/operators/unwrap-optional.d.mts +44 -0
- package/dist/core/predefined/operators/unwrap-optional.d.mts.map +1 -0
- package/dist/core/predefined/operators/unwrap-optional.mjs +50 -0
- package/dist/core/predefined/operators/unwrap-optional.mjs.map +1 -0
- package/dist/core/predefined/operators/unwrap-result-err.d.mts +44 -0
- package/dist/core/predefined/operators/unwrap-result-err.d.mts.map +1 -0
- package/dist/core/predefined/operators/unwrap-result-err.mjs +48 -0
- package/dist/core/predefined/operators/unwrap-result-err.mjs.map +1 -0
- package/dist/core/predefined/operators/unwrap-result-ok.d.mts +44 -0
- package/dist/core/predefined/operators/unwrap-result-ok.d.mts.map +1 -0
- package/dist/core/predefined/operators/unwrap-result-ok.mjs +50 -0
- package/dist/core/predefined/operators/unwrap-result-ok.mjs.map +1 -0
- package/dist/core/types/id.d.mts +1 -1
- package/dist/core/types/id.d.mts.map +1 -1
- package/dist/core/types/index.d.mts +1 -0
- package/dist/core/types/index.d.mts.map +1 -1
- package/dist/core/types/observable-family.d.mts +8 -14
- package/dist/core/types/observable-family.d.mts.map +1 -1
- package/dist/core/types/observable.d.mts +3 -3
- package/dist/core/types/observable.d.mts.map +1 -1
- package/dist/core/types/timer.d.mts +2 -0
- package/dist/core/types/timer.d.mts.map +1 -0
- package/dist/core/types/timer.mjs +2 -0
- package/dist/core/types/timer.mjs.map +1 -0
- package/dist/core/utils/id-maker.d.mts +2 -2
- package/dist/core/utils/id-maker.d.mts.map +1 -1
- package/dist/core/utils/id-maker.mjs +3 -3
- package/dist/core/utils/id-maker.mjs.map +1 -1
- package/dist/core/utils/index.mjs +1 -1
- package/dist/entry-point.mjs +24 -15
- package/dist/entry-point.mjs.map +1 -1
- package/dist/globals.d.mts +0 -3
- package/dist/index.mjs +24 -15
- package/dist/index.mjs.map +1 -1
- package/dist/utils/collect-to-array.d.mts +3 -0
- package/dist/utils/collect-to-array.d.mts.map +1 -0
- package/dist/utils/collect-to-array.mjs +11 -0
- package/dist/utils/collect-to-array.mjs.map +1 -0
- package/dist/utils/create-boolean-state.d.mts +40 -0
- package/dist/utils/create-boolean-state.d.mts.map +1 -0
- package/dist/utils/create-boolean-state.mjs +53 -0
- package/dist/utils/create-boolean-state.mjs.map +1 -0
- package/dist/utils/create-event-emitter.d.mts +4 -4
- package/dist/utils/create-event-emitter.mjs +4 -4
- package/dist/utils/create-reducer.d.mts +10 -7
- package/dist/utils/create-reducer.d.mts.map +1 -1
- package/dist/utils/create-reducer.mjs +7 -7
- package/dist/utils/create-reducer.mjs.map +1 -1
- package/dist/utils/create-state.d.mts +8 -48
- package/dist/utils/create-state.d.mts.map +1 -1
- package/dist/utils/create-state.mjs +10 -60
- package/dist/utils/create-state.mjs.map +1 -1
- package/dist/utils/index.d.mts +2 -0
- package/dist/utils/index.d.mts.map +1 -1
- package/dist/utils/index.mjs +3 -1
- package/dist/utils/index.mjs.map +1 -1
- package/package.json +17 -11
- package/src/core/class/child-observable-class.mts +65 -9
- package/src/core/class/circular-dependency-comparison.test.mts +142 -0
- package/src/core/class/circular-dependency.test.mts +251 -0
- package/src/core/class/observable-base-class.mts +9 -9
- package/src/core/class/root-observable-class.mts +14 -10
- package/src/core/combine/combine.mts +15 -15
- package/src/core/combine/merge.mts +13 -14
- package/src/core/combine/zip.mts +26 -25
- package/src/core/create/{interval.mts → counter.mts} +32 -30
- package/src/core/create/from-abortable-promise.mts +83 -0
- package/src/core/create/from-promise.mts +10 -7
- package/src/core/create/from-subscribable.mts +4 -4
- package/src/core/create/index.mts +3 -3
- package/src/core/create/just.mts +43 -0
- package/src/core/create/source.mts +10 -14
- package/src/core/create/timer.mts +12 -11
- package/src/core/index.mts +1 -1
- package/src/core/operators/audit.mts +172 -0
- package/src/core/operators/debounce.mts +154 -0
- package/src/core/operators/filter.mts +9 -9
- package/src/core/operators/index.mts +4 -4
- package/src/core/operators/map.mts +124 -0
- package/src/core/operators/merge-map.mts +60 -33
- package/src/core/operators/pairwise.mts +10 -10
- package/src/core/operators/scan.mts +10 -10
- package/src/core/operators/skip-if-no-change.mts +26 -14
- package/src/core/operators/skip-until.mts +9 -9
- package/src/core/operators/skip-while.mts +30 -28
- package/src/core/operators/switch-map.mts +60 -29
- package/src/core/operators/take-until.mts +9 -9
- package/src/core/operators/take-while.mts +21 -19
- package/src/core/operators/{throttle-time.mts → throttle.mts} +58 -38
- package/src/core/operators/with-buffered-from.mts +18 -14
- package/src/core/operators/with-current-value-from.mts +19 -14
- package/src/core/operators/with-initial-value.mts +9 -9
- package/src/core/predefined/index.mts +1 -0
- package/src/core/predefined/operators/attach-index.mts +62 -0
- package/src/core/predefined/operators/index.mts +11 -0
- package/src/core/predefined/operators/map-optional.mts +55 -0
- package/src/core/predefined/operators/map-result-err.mts +55 -0
- package/src/core/predefined/operators/map-result-ok.mts +55 -0
- package/src/core/predefined/operators/map-to.mts +45 -0
- package/src/core/predefined/operators/pluck.mts +51 -0
- package/src/core/predefined/operators/skip.mts +57 -0
- package/src/core/predefined/operators/take.mts +47 -0
- package/src/core/predefined/operators/unwrap-optional.mts +49 -0
- package/src/core/predefined/operators/unwrap-result-err.mts +48 -0
- package/src/core/predefined/operators/unwrap-result-ok.mts +49 -0
- package/src/core/types/id.mts +1 -1
- package/src/core/types/index.mts +1 -0
- package/src/core/types/observable-family.mts +8 -24
- package/src/core/types/observable.mts +3 -3
- package/src/core/types/timer.mts +2 -0
- package/src/core/utils/id-maker.mts +4 -4
- package/src/globals.d.mts +0 -3
- package/src/utils/collect-to-array.mts +17 -0
- package/src/utils/create-boolean-state.mts +68 -0
- package/src/utils/create-event-emitter.mts +4 -4
- package/src/utils/create-reducer.mts +11 -8
- package/src/utils/create-state.mts +10 -75
- package/src/utils/index.mts +2 -0
- package/dist/core/create/from-array.d.mts +0 -39
- package/dist/core/create/from-array.d.mts.map +0 -1
- package/dist/core/create/from-array.mjs +0 -65
- package/dist/core/create/from-array.mjs.map +0 -1
- package/dist/core/create/interval.d.mts.map +0 -1
- package/dist/core/create/interval.mjs.map +0 -1
- package/dist/core/create/of.d.mts +0 -39
- package/dist/core/create/of.d.mts.map +0 -1
- package/dist/core/create/of.mjs +0 -63
- package/dist/core/create/of.mjs.map +0 -1
- package/dist/core/operators/audit-time.d.mts +0 -62
- package/dist/core/operators/audit-time.d.mts.map +0 -1
- package/dist/core/operators/audit-time.mjs +0 -109
- package/dist/core/operators/audit-time.mjs.map +0 -1
- package/dist/core/operators/debounce-time.d.mts +0 -51
- package/dist/core/operators/debounce-time.d.mts.map +0 -1
- package/dist/core/operators/debounce-time.mjs +0 -93
- package/dist/core/operators/debounce-time.mjs.map +0 -1
- package/dist/core/operators/map-with-index.d.mts +0 -54
- package/dist/core/operators/map-with-index.d.mts.map +0 -1
- package/dist/core/operators/map-with-index.mjs +0 -88
- package/dist/core/operators/map-with-index.mjs.map +0 -1
- package/dist/core/operators/throttle-time.d.mts +0 -62
- package/dist/core/operators/throttle-time.d.mts.map +0 -1
- package/dist/core/operators/throttle-time.mjs +0 -107
- package/dist/core/operators/throttle-time.mjs.map +0 -1
- package/src/core/create/from-array.mts +0 -76
- package/src/core/create/of.mts +0 -73
- package/src/core/operators/audit-time.mts +0 -136
- package/src/core/operators/debounce-time.mts +0 -116
- package/src/core/operators/map-with-index.mts +0 -183
package/README.md
CHANGED
|
@@ -9,26 +9,30 @@
|
|
|
9
9
|
[](https://www.npmjs.com/package/synstate)
|
|
10
10
|
[](https://www.npmjs.com/package/synstate)
|
|
11
11
|
[](./LICENSE)
|
|
12
|
-
[](https://codecov.io/gh/noshiro-pf/synstate)
|
|
13
13
|
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
|
-
**SynState** is a lightweight, high-performance, type-safe state management library for TypeScript/JavaScript. Perfect for building reactive global state and event-driven systems in React, Vue, and other frameworks.
|
|
16
|
+
**SynState** is a lightweight, high-performance, type-safe state management library for TypeScript/JavaScript applications. Perfect for building reactive global state and event-driven systems in React, Vue, and other frameworks.
|
|
17
|
+
|
|
18
|
+
"SynState" is named after "Synchronized + State." It represents a sound synchronized state through a **glitch-free**[^1] Observable implementation.
|
|
19
|
+
|
|
20
|
+
[^1]: See ["How SynState solved the glitch?"](./documents/how-synstate-solved-the-glitch.md).
|
|
17
21
|
|
|
18
22
|
## Features
|
|
19
23
|
|
|
20
|
-
- 🎯 **Simple State Management**: Easy-to-use `createState` and `createReducer` for global state
|
|
21
|
-
- 📡 **Event System**: Built-in `createValueEmitter`, `createEventEmitter` for event-driven architecture
|
|
22
|
-
- 🔄 **Reactive Updates**: Automatic propagation of state changes to subscribers
|
|
23
|
-
- 🎨 **Type-Safe**: Full TypeScript support with precise type inference
|
|
24
|
-
- 🚀 **Lightweight**: Minimal bundle size with only one external runtime dependency ([ts-data-forge](https://www.npmjs.com/package/ts-data-forge))
|
|
24
|
+
- 🎯 **Simple State Management**: Easy-to-use `createState` and `createReducer` similar to React useState/useReducer for global state
|
|
25
25
|
- ⚡ **High Performance**: Optimized for fast state updates and minimal re-renders
|
|
26
|
+
- 🎨 **Type-Safe**: Full TypeScript support with precise type inference
|
|
27
|
+
- 🚀 **Lightweight**: <!-- bundle-size:synstate -->~4.5 kB min+gzip<!-- /bundle-size:synstate --> with only one external runtime dependency ([ts-data-forge](https://www.npmjs.com/package/ts-data-forge))
|
|
26
28
|
- 🌐 **Framework Agnostic**: Works with React, Vue, Svelte, or vanilla JavaScript
|
|
27
|
-
-
|
|
29
|
+
- 🔄 **Reactive Updates**: Automatic propagation of state changes to all subscribers
|
|
30
|
+
- 📡 **Event System**: Built-in `createValueEmitter`, `createEventEmitter` for event-driven architecture
|
|
31
|
+
- 🔧 **Observable-based**: Built on Observable pattern similar to RxJS, but with a completely independent implementation from scratch — not a wrapper. Offers optional advanced features like operators (`map`, `filter`, `scan`, `debounce`) and combinators (`merge`, `combine`)
|
|
28
32
|
|
|
29
33
|
## Documentation
|
|
30
34
|
|
|
31
|
-
-
|
|
35
|
+
- <https://noshiro-pf.github.io/synstate/>
|
|
32
36
|
|
|
33
37
|
## Installation
|
|
34
38
|
|
|
@@ -52,28 +56,103 @@ pnpm add synstate
|
|
|
52
56
|
|
|
53
57
|
```tsx
|
|
54
58
|
// Create a reactive state
|
|
55
|
-
const [state, setState
|
|
59
|
+
const [state, setState] = createState(0);
|
|
60
|
+
// type of state: InitializedObservable<number>
|
|
61
|
+
// type of setState: (v: number) => number
|
|
56
62
|
|
|
57
|
-
const
|
|
63
|
+
const stateHistory: number[] = [];
|
|
58
64
|
|
|
59
|
-
// Subscribe to changes
|
|
60
|
-
state.subscribe((count
|
|
61
|
-
|
|
65
|
+
// Subscribe to changes
|
|
66
|
+
state.subscribe((count) => {
|
|
67
|
+
stateHistory.push(count);
|
|
62
68
|
});
|
|
63
69
|
|
|
64
|
-
assert.deepStrictEqual(
|
|
70
|
+
assert.deepStrictEqual(stateHistory, [0]);
|
|
65
71
|
|
|
66
72
|
// Update state
|
|
67
73
|
setState(1);
|
|
68
74
|
|
|
69
|
-
assert.deepStrictEqual(
|
|
75
|
+
assert.deepStrictEqual(stateHistory, [0, 1]);
|
|
76
|
+
```
|
|
70
77
|
|
|
71
|
-
|
|
78
|
+
### With React
|
|
72
79
|
|
|
73
|
-
|
|
80
|
+
```bash
|
|
81
|
+
npm add synstate-react-hooks
|
|
74
82
|
```
|
|
75
83
|
|
|
76
|
-
|
|
84
|
+
```tsx
|
|
85
|
+
import type * as React from 'react';
|
|
86
|
+
import { createState } from 'synstate-react-hooks';
|
|
87
|
+
|
|
88
|
+
const [useUserState, setUserState] = createState({
|
|
89
|
+
name: '',
|
|
90
|
+
email: '',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const UserProfile = (): React.JSX.Element => {
|
|
94
|
+
const user = useUserState();
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div>
|
|
98
|
+
<p>{`Name: ${user.name}`}</p>
|
|
99
|
+
<button
|
|
100
|
+
onClick={() => {
|
|
101
|
+
setUserState({
|
|
102
|
+
name: 'Alice',
|
|
103
|
+
email: 'alice@example.com',
|
|
104
|
+
});
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
{'Set User'}
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This is equivalent to the following code without synstate-react-hook:
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import * as React from 'react';
|
|
118
|
+
import { createState } from 'synstate';
|
|
119
|
+
|
|
120
|
+
const [userState, setUserState] = createState({
|
|
121
|
+
name: '',
|
|
122
|
+
email: '',
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const UserProfile = (): React.JSX.Element => {
|
|
126
|
+
const user = React.useSyncExternalStore(
|
|
127
|
+
(onStoreChange: () => void) => {
|
|
128
|
+
const { unsubscribe } = userState.subscribe(onStoreChange);
|
|
129
|
+
|
|
130
|
+
return unsubscribe;
|
|
131
|
+
},
|
|
132
|
+
() => userState.getSnapshot().value,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div>
|
|
137
|
+
<p>{`Name: ${user.name}`}</p>
|
|
138
|
+
<button
|
|
139
|
+
onClick={() => {
|
|
140
|
+
setUserState({
|
|
141
|
+
name: 'Alice',
|
|
142
|
+
email: 'alice@example.com',
|
|
143
|
+
});
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
{'Set User'}
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
See also the [synstate-react-hooks README](../synstate-react-hooks/README.md).
|
|
154
|
+
|
|
155
|
+
If you're using React v17 or earlier:
|
|
77
156
|
|
|
78
157
|
```tsx
|
|
79
158
|
import * as React from 'react';
|
|
@@ -98,10 +177,7 @@ const UserProfile = (): React.JSX.Element => {
|
|
|
98
177
|
|
|
99
178
|
return (
|
|
100
179
|
<div>
|
|
101
|
-
<p>
|
|
102
|
-
{'Name: '}
|
|
103
|
-
{user.name}
|
|
104
|
-
</p>
|
|
180
|
+
<p>{`Name: ${user.name}`}</p>
|
|
105
181
|
<button
|
|
106
182
|
onClick={() => {
|
|
107
183
|
setUserState({
|
|
@@ -117,207 +193,135 @@ const UserProfile = (): React.JSX.Element => {
|
|
|
117
193
|
};
|
|
118
194
|
```
|
|
119
195
|
|
|
120
|
-
##
|
|
121
|
-
|
|
122
|
-
### State Management
|
|
196
|
+
## Why SynState?
|
|
123
197
|
|
|
124
|
-
|
|
198
|
+
### Simple to Start, Powerful When You Need It
|
|
125
199
|
|
|
126
|
-
|
|
127
|
-
- **`createReducer`**: Redux-style state management
|
|
128
|
-
- **`createBooleanState`**: Specialized state for boolean values
|
|
200
|
+
SynState is a state management library for web frontends. For most use cases, `createState`, `createReducer`, and simple combinators like `combine` and `map` are all you need — clean, minimal APIs that feel as intuitive as React's `useState` / `useReducer`, but for global state.
|
|
129
201
|
|
|
130
|
-
|
|
202
|
+
When your requirements grow more complex, SynState scales with you. Built on its own Observable implementation, it provides operators like `debounce`, `throttle`, `switchMap`, and `mergeMap` for sophisticated asynchronous state management — without requiring an additional library like RxJS. You can describe everything from a simple counter to a debounced search pipeline with auto-cancellation in a single, unified API.
|
|
131
203
|
|
|
132
|
-
|
|
204
|
+
### Why Observable-Based?
|
|
133
205
|
|
|
134
|
-
|
|
135
|
-
- **`createEventEmitter`**: Create event emitters without payload
|
|
206
|
+
A state management library that scales from simple global state to complex asynchronous workflows needs **reactive value propagation** at its core — when one piece of state changes, all derived values must update automatically and consistently. The Observable pattern is a natural fit for this: it models state as streams of values that can be composed, transformed, and combined declaratively.
|
|
136
207
|
|
|
137
|
-
|
|
208
|
+
RxJS is the most well-known Observable library, and it excels at modeling asynchronous event processing. However, RxJS has a fundamental issue known as **glitch**[^1] — a phenomenon where derived values can temporarily enter inconsistent intermediate states during synchronous propagation. For a state management library, where consistency of derived state is critical, this is unacceptable. SynState was built from scratch with a glitch-free Observable implementation to solve this problem.
|
|
138
209
|
|
|
139
|
-
For
|
|
210
|
+
For a detailed explanation, see ["How SynState solved the glitch?"](./documents/how-synstate-solved-the-glitch.md).
|
|
140
211
|
|
|
141
|
-
|
|
212
|
+
### Key Differences from RxJS
|
|
142
213
|
|
|
143
|
-
|
|
214
|
+
- **Glitch free**: While RxJS Observables suffer from a troublesome phenomenon called glitch [^1], SynState Observables are glitch-free.
|
|
215
|
+
- **InitializedObservable**: Provides `InitializedObservable` which always holds an initial value, making it ideal for representing state
|
|
216
|
+
- **Focus on State Management**: Designed specifically for state management, not just asynchronous event processing. SynState provides utility functions `createState`, `createReducer`, and `createBooleanState`. However, this doesn't mean it's inadequate for asynchronous event processing — it can handle asynchronous operations as elegantly as RxJS.
|
|
144
217
|
|
|
145
|
-
|
|
218
|
+
### Use Cases
|
|
146
219
|
|
|
147
|
-
|
|
220
|
+
**Use SynState when you need:**
|
|
148
221
|
|
|
149
|
-
|
|
222
|
+
- ✅ A small piece of global state shared across components (e.g., dark mode toggle, user session)
|
|
223
|
+
- ✅ Complex asynchronous state management with operators like `debounce`, `throttle`, `switchMap`
|
|
224
|
+
- ✅ Redux-like state with reducers (`createReducer`)
|
|
225
|
+
- ✅ A project where the scale of state management is uncertain — SynState's unified API covers everything from a single shared counter to a full debounced search pipeline, so you never have to switch libraries as requirements grow
|
|
226
|
+
- ✅ Type-safe event emitters (`createEventEmitter`)
|
|
150
227
|
|
|
151
|
-
|
|
228
|
+
**Consider other solutions when:**
|
|
152
229
|
|
|
153
|
-
|
|
230
|
+
- You only need a React component (local) state (use React hooks `useState`, `useReducer`)
|
|
154
231
|
|
|
155
|
-
|
|
232
|
+
## Examples
|
|
156
233
|
|
|
157
|
-
###
|
|
234
|
+
### Simple State with Additional APIs
|
|
158
235
|
|
|
159
|
-
|
|
236
|
+
```tsx
|
|
237
|
+
// Create a reactive state
|
|
238
|
+
const [
|
|
239
|
+
state,
|
|
240
|
+
setState,
|
|
241
|
+
{ updateState, resetState, getSnapshot, initialState },
|
|
242
|
+
] = createState(0);
|
|
243
|
+
// type of state: InitializedObservable<number>
|
|
244
|
+
// type of setState: (v: number) => number
|
|
245
|
+
// type of updateState: (updater: (prev: number) => number) => number
|
|
246
|
+
// type of resetState: () => void
|
|
247
|
+
// type of getSnapshot: () => number
|
|
248
|
+
// type of initialState: number
|
|
249
|
+
|
|
250
|
+
const stateHistory: number[] = [];
|
|
251
|
+
|
|
252
|
+
// Subscribe to changes
|
|
253
|
+
state.subscribe((count) => {
|
|
254
|
+
stateHistory.push(count);
|
|
255
|
+
});
|
|
160
256
|
|
|
161
|
-
|
|
257
|
+
assert.deepStrictEqual(stateHistory, [0]);
|
|
162
258
|
|
|
163
|
-
|
|
259
|
+
assert.strictEqual(getSnapshot(), 0);
|
|
164
260
|
|
|
165
|
-
|
|
166
|
-
|
|
261
|
+
// Update state
|
|
262
|
+
setState(1);
|
|
167
263
|
|
|
168
|
-
|
|
264
|
+
assert.strictEqual(getSnapshot(), 1);
|
|
169
265
|
|
|
170
|
-
|
|
266
|
+
assert.deepStrictEqual(stateHistory, [0, 1]);
|
|
171
267
|
|
|
172
|
-
|
|
268
|
+
updateState((prev) => prev + 2);
|
|
173
269
|
|
|
174
|
-
|
|
175
|
-
- `of(value)`: Create observable from a single value
|
|
176
|
-
- `fromArray(array)`: Create observable from array
|
|
177
|
-
- `fromPromise(promise)`: Create observable from promise
|
|
178
|
-
- `interval(ms)`: Emit values at intervals
|
|
179
|
-
- `timer(delay)`: Emit after delay
|
|
270
|
+
assert.strictEqual(getSnapshot(), 3);
|
|
180
271
|
|
|
181
|
-
|
|
272
|
+
assert.deepStrictEqual(stateHistory, [0, 1, 3]);
|
|
182
273
|
|
|
183
|
-
|
|
184
|
-
- `map(fn)`: Transform values
|
|
185
|
-
- `scan(reducer, seed)`: Accumulate values
|
|
186
|
-
- `debounceTime(ms)`: Debounce emissions
|
|
187
|
-
- `throttleTime(ms)`: Throttle emissions
|
|
188
|
-
- `skipIfNoChange()`: Skip duplicate values
|
|
189
|
-
- `takeUntil(notifier)`: Complete on notifier emission
|
|
274
|
+
resetState();
|
|
190
275
|
|
|
191
|
-
|
|
276
|
+
assert.strictEqual(getSnapshot(), 0);
|
|
192
277
|
|
|
193
|
-
|
|
194
|
-
- `merge(observables)`: Merge multiple streams
|
|
195
|
-
- `zip(observables)`: Pair values by index
|
|
278
|
+
assert.strictEqual(initialState, 0);
|
|
196
279
|
|
|
197
|
-
|
|
280
|
+
assert.deepStrictEqual(stateHistory, [0, 1, 3, 0]);
|
|
281
|
+
```
|
|
198
282
|
|
|
199
283
|
### Global Counter State (React)
|
|
200
284
|
|
|
201
285
|
```tsx
|
|
202
|
-
import * as React from 'react';
|
|
203
|
-
import { createState } from 'synstate';
|
|
286
|
+
import type * as React from 'react';
|
|
287
|
+
import { createState } from 'synstate-react-hooks';
|
|
204
288
|
|
|
205
289
|
// Create global state
|
|
206
|
-
export const [
|
|
207
|
-
|
|
290
|
+
export const [useCounterState, , { updateState, resetState }] = createState(0);
|
|
291
|
+
|
|
292
|
+
const increment = (): void => {
|
|
293
|
+
updateState((n) => n + 1);
|
|
294
|
+
};
|
|
208
295
|
|
|
209
296
|
// Component 1
|
|
210
297
|
const Counter = (): React.JSX.Element => {
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
React.useEffect(() => {
|
|
214
|
-
const sub = counterState.subscribe(setCount);
|
|
215
|
-
|
|
216
|
-
return () => {
|
|
217
|
-
sub.unsubscribe();
|
|
218
|
-
};
|
|
219
|
-
}, []);
|
|
298
|
+
const count = useCounterState();
|
|
220
299
|
|
|
221
300
|
return (
|
|
222
301
|
<div>
|
|
223
|
-
<p>
|
|
224
|
-
|
|
225
|
-
{count}
|
|
226
|
-
</p>
|
|
227
|
-
<button
|
|
228
|
-
onClick={() => {
|
|
229
|
-
updateState((n: number) => n + 1);
|
|
230
|
-
}}
|
|
231
|
-
>
|
|
232
|
-
{'Increment'}
|
|
233
|
-
</button>
|
|
302
|
+
<p>{`Count: ${count}`}</p>
|
|
303
|
+
<button onClick={increment}>{'Increment'}</button>
|
|
234
304
|
</div>
|
|
235
305
|
);
|
|
236
306
|
};
|
|
237
307
|
|
|
238
|
-
// Component 2
|
|
308
|
+
// Component 2
|
|
239
309
|
const ResetButton = (): React.JSX.Element => (
|
|
240
|
-
<button
|
|
241
|
-
onClick={() => {
|
|
242
|
-
resetState();
|
|
243
|
-
}}
|
|
244
|
-
>
|
|
245
|
-
{'Reset'}
|
|
246
|
-
</button>
|
|
310
|
+
<button onClick={resetState}>{'Reset'}</button>
|
|
247
311
|
);
|
|
248
312
|
```
|
|
249
313
|
|
|
250
|
-
### Event-Driven Architecture (React)
|
|
251
|
-
|
|
252
|
-
```tsx
|
|
253
|
-
import * as React from 'react';
|
|
254
|
-
import { createEventEmitter, createValueEmitter } from 'synstate';
|
|
255
|
-
|
|
256
|
-
// Global events
|
|
257
|
-
export const [userLoggedIn$, emitUserLoggedIn] = createValueEmitter<
|
|
258
|
-
Readonly<{
|
|
259
|
-
id: number;
|
|
260
|
-
name: string;
|
|
261
|
-
}>
|
|
262
|
-
>();
|
|
263
|
-
|
|
264
|
-
export const [userLoggedOut$, emitUserLoggedOut] = createEventEmitter();
|
|
265
|
-
|
|
266
|
-
// Component that emits events
|
|
267
|
-
const LoginButton = (): React.JSX.Element => {
|
|
268
|
-
const handleLogin = React.useCallback(() => {
|
|
269
|
-
(async () => {
|
|
270
|
-
const user = await loginUser();
|
|
271
|
-
|
|
272
|
-
emitUserLoggedIn(user);
|
|
273
|
-
})().catch(() => {});
|
|
274
|
-
}, []);
|
|
275
|
-
|
|
276
|
-
return <button onClick={handleLogin}>{'Login'}</button>;
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
// Component that listens to events
|
|
280
|
-
const NotificationPage = (): React.JSX.Element => {
|
|
281
|
-
const [message, setMessage] = React.useState('');
|
|
282
|
-
|
|
283
|
-
React.useEffect(() => {
|
|
284
|
-
const sub1 = userLoggedIn$.subscribe((user) => {
|
|
285
|
-
setMessage(`Welcome, ${user.name}!`);
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
const sub2 = userLoggedOut$.subscribe(() => {
|
|
289
|
-
setMessage('Logged out');
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
return () => {
|
|
293
|
-
sub1.unsubscribe();
|
|
294
|
-
|
|
295
|
-
sub2.unsubscribe();
|
|
296
|
-
};
|
|
297
|
-
}, []);
|
|
298
|
-
|
|
299
|
-
return message !== '' ? (
|
|
300
|
-
<div className={'notification'}>{message}</div>
|
|
301
|
-
) : (
|
|
302
|
-
<>{null}</>
|
|
303
|
-
);
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
const loginUser = async (): Promise<
|
|
307
|
-
Readonly<{
|
|
308
|
-
id: number;
|
|
309
|
-
name: string;
|
|
310
|
-
}>
|
|
311
|
-
> => ({ id: 1, name: 'Alice' });
|
|
312
|
-
```
|
|
313
|
-
|
|
314
314
|
### Todo List with Reducer (React)
|
|
315
315
|
|
|
316
316
|
```tsx
|
|
317
317
|
import * as React from 'react';
|
|
318
|
-
import { createReducer } from 'synstate';
|
|
318
|
+
import { createReducer } from 'synstate-react-hooks';
|
|
319
319
|
|
|
320
|
-
type Todo = Readonly<{
|
|
320
|
+
type Todo = Readonly<{
|
|
321
|
+
id: number;
|
|
322
|
+
text: string;
|
|
323
|
+
done: boolean;
|
|
324
|
+
}>;
|
|
321
325
|
|
|
322
326
|
type Action = Readonly<
|
|
323
327
|
| { type: 'add'; text: string }
|
|
@@ -325,10 +329,9 @@ type Action = Readonly<
|
|
|
325
329
|
| { type: 'remove'; id: number }
|
|
326
330
|
>;
|
|
327
331
|
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
>((todos, action) => {
|
|
332
|
+
const initialTodos: readonly Todo[] = [] as const;
|
|
333
|
+
|
|
334
|
+
const reducer = (todos: readonly Todo[], action: Action): readonly Todo[] => {
|
|
332
335
|
switch (action.type) {
|
|
333
336
|
case 'add':
|
|
334
337
|
return [
|
|
@@ -339,53 +342,66 @@ const [todoState, dispatch, getSnapshot] = createReducer<
|
|
|
339
342
|
done: false,
|
|
340
343
|
},
|
|
341
344
|
];
|
|
345
|
+
|
|
342
346
|
case 'toggle':
|
|
343
347
|
return todos.map((t) =>
|
|
344
348
|
t.id === action.id ? { ...t, done: !t.done } : t,
|
|
345
349
|
);
|
|
350
|
+
|
|
346
351
|
case 'remove':
|
|
347
352
|
return todos.filter((t) => t.id !== action.id);
|
|
348
353
|
}
|
|
349
|
-
}
|
|
354
|
+
};
|
|
350
355
|
|
|
351
|
-
const
|
|
352
|
-
|
|
356
|
+
const [useTodoState, dispatch] = createReducer<readonly Todo[], Action>(
|
|
357
|
+
reducer,
|
|
358
|
+
initialTodos,
|
|
359
|
+
);
|
|
353
360
|
|
|
354
|
-
|
|
355
|
-
|
|
361
|
+
const addTodo = (): void => {
|
|
362
|
+
dispatch({
|
|
363
|
+
type: 'add',
|
|
364
|
+
text: 'New Todo',
|
|
365
|
+
});
|
|
366
|
+
};
|
|
356
367
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
368
|
+
const TodoList = (): React.JSX.Element => {
|
|
369
|
+
const todos = useTodoState();
|
|
370
|
+
|
|
371
|
+
const todosWithHandler = React.useMemo(
|
|
372
|
+
() =>
|
|
373
|
+
todos.map((todo) => ({
|
|
374
|
+
...todo,
|
|
375
|
+
onToggle: () => {
|
|
376
|
+
dispatch({
|
|
377
|
+
type: 'toggle',
|
|
378
|
+
id: todo.id,
|
|
379
|
+
});
|
|
380
|
+
},
|
|
381
|
+
onRemove: () => {
|
|
382
|
+
dispatch({
|
|
383
|
+
type: 'remove',
|
|
384
|
+
id: todo.id,
|
|
385
|
+
});
|
|
386
|
+
},
|
|
387
|
+
})),
|
|
388
|
+
[todos],
|
|
389
|
+
);
|
|
361
390
|
|
|
362
391
|
return (
|
|
363
392
|
<div>
|
|
364
|
-
{
|
|
393
|
+
{todosWithHandler.map((todo) => (
|
|
365
394
|
<div key={todo.id}>
|
|
366
395
|
<input
|
|
367
396
|
checked={todo.done}
|
|
368
397
|
type={'checkbox'}
|
|
369
|
-
onChange={
|
|
370
|
-
dispatch({
|
|
371
|
-
type: 'toggle',
|
|
372
|
-
id: todo.id,
|
|
373
|
-
});
|
|
374
|
-
}}
|
|
398
|
+
onChange={todo.onToggle}
|
|
375
399
|
/>
|
|
376
400
|
<span>{todo.text}</span>
|
|
401
|
+
<button onClick={todo.onRemove}>{'Remove'}</button>
|
|
377
402
|
</div>
|
|
378
403
|
))}
|
|
379
|
-
<button
|
|
380
|
-
onClick={() => {
|
|
381
|
-
dispatch({
|
|
382
|
-
type: 'add',
|
|
383
|
-
text: 'New Todo',
|
|
384
|
-
});
|
|
385
|
-
}}
|
|
386
|
-
>
|
|
387
|
-
{'Add Todo'}
|
|
388
|
-
</button>
|
|
404
|
+
<button onClick={addTodo}>{'Add Todo'}</button>
|
|
389
405
|
</div>
|
|
390
406
|
);
|
|
391
407
|
};
|
|
@@ -395,35 +411,19 @@ const TodoList = (): React.JSX.Element => {
|
|
|
395
411
|
|
|
396
412
|
```tsx
|
|
397
413
|
import * as React from 'react';
|
|
398
|
-
import { createBooleanState } from 'synstate';
|
|
414
|
+
import { createBooleanState } from 'synstate-react-hooks';
|
|
399
415
|
|
|
400
|
-
export const [
|
|
416
|
+
export const [useDarkModeState, { toggle: toggleDarkMode }] =
|
|
401
417
|
createBooleanState(false);
|
|
402
418
|
|
|
403
419
|
const ThemeToggle = (): React.JSX.Element => {
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
React.useEffect(() => {
|
|
407
|
-
const sub = darkModeState.subscribe(setIsDark);
|
|
408
|
-
|
|
409
|
-
return () => {
|
|
410
|
-
sub.unsubscribe();
|
|
411
|
-
};
|
|
412
|
-
}, []);
|
|
420
|
+
const isDark = useDarkModeState();
|
|
413
421
|
|
|
414
422
|
React.useEffect(() => {
|
|
415
423
|
document.body.className = isDark ? 'dark' : 'light';
|
|
416
424
|
}, [isDark]);
|
|
417
425
|
|
|
418
|
-
return
|
|
419
|
-
<button
|
|
420
|
-
onClick={() => {
|
|
421
|
-
toggle();
|
|
422
|
-
}}
|
|
423
|
-
>
|
|
424
|
-
{isDark ? '🌙' : '☀️'}
|
|
425
|
-
</button>
|
|
426
|
-
);
|
|
426
|
+
return <button onClick={toggleDarkMode}>{isDark ? '🌙' : '☀️'}</button>;
|
|
427
427
|
};
|
|
428
428
|
```
|
|
429
429
|
|
|
@@ -431,30 +431,20 @@ const ThemeToggle = (): React.JSX.Element => {
|
|
|
431
431
|
|
|
432
432
|
```tsx
|
|
433
433
|
import * as React from 'react';
|
|
434
|
-
import {
|
|
435
|
-
|
|
436
|
-
// Events
|
|
437
|
-
const [onItemAdded$, emitItemAdded] = createValueEmitter<string>();
|
|
438
|
-
|
|
439
|
-
const [onClearAll$, emitClearAll] = createEventEmitter();
|
|
434
|
+
import { createState } from 'synstate-react-hooks';
|
|
440
435
|
|
|
441
436
|
// State
|
|
442
|
-
const [
|
|
443
|
-
readonly string[]
|
|
444
|
-
>([]);
|
|
437
|
+
const [useItemsState, _, { updateState, resetState: resetItemsState }] =
|
|
438
|
+
createState<readonly string[]>([]);
|
|
445
439
|
|
|
446
440
|
// Setup event handlers
|
|
447
|
-
|
|
441
|
+
const addItem = (item: string): void => {
|
|
448
442
|
updateState((items: readonly string[]) => [...items, item]);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
onClearAll$.subscribe(() => {
|
|
452
|
-
setItemsState([]);
|
|
453
|
-
});
|
|
443
|
+
};
|
|
454
444
|
|
|
455
445
|
// Component 1: Add items
|
|
456
446
|
const ItemInput = (): React.JSX.Element => {
|
|
457
|
-
const [input, setInput] = React.useState('');
|
|
447
|
+
const [input, setInput] = React.useState<string>('');
|
|
458
448
|
|
|
459
449
|
return (
|
|
460
450
|
<div>
|
|
@@ -466,7 +456,7 @@ const ItemInput = (): React.JSX.Element => {
|
|
|
466
456
|
/>
|
|
467
457
|
<button
|
|
468
458
|
onClick={() => {
|
|
469
|
-
|
|
459
|
+
addItem(input);
|
|
470
460
|
|
|
471
461
|
setInput('');
|
|
472
462
|
}}
|
|
@@ -479,15 +469,7 @@ const ItemInput = (): React.JSX.Element => {
|
|
|
479
469
|
|
|
480
470
|
// Component 2: Display items
|
|
481
471
|
const ItemList = (): React.JSX.Element => {
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
React.useEffect(() => {
|
|
485
|
-
const sub = itemsState.subscribe(setItems);
|
|
486
|
-
|
|
487
|
-
return () => {
|
|
488
|
-
sub.unsubscribe();
|
|
489
|
-
};
|
|
490
|
-
}, []);
|
|
472
|
+
const items = useItemsState();
|
|
491
473
|
|
|
492
474
|
return (
|
|
493
475
|
<div>
|
|
@@ -496,40 +478,41 @@ const ItemList = (): React.JSX.Element => {
|
|
|
496
478
|
<li key={i}>{item}</li>
|
|
497
479
|
))}
|
|
498
480
|
</ul>
|
|
499
|
-
<button onClick={
|
|
481
|
+
<button onClick={resetItemsState}>{'Clear All'}</button>
|
|
500
482
|
</div>
|
|
501
483
|
);
|
|
502
484
|
};
|
|
503
485
|
```
|
|
504
486
|
|
|
505
|
-
// Events
|
|
506
|
-
|
|
507
487
|
### Advanced: Search with Debounce
|
|
508
488
|
|
|
509
489
|
```tsx
|
|
510
|
-
import * as React from 'react';
|
|
490
|
+
import type * as React from 'react';
|
|
511
491
|
import {
|
|
512
492
|
createState,
|
|
513
|
-
|
|
493
|
+
debounce,
|
|
514
494
|
filter,
|
|
515
|
-
|
|
516
|
-
type
|
|
495
|
+
fromAbortablePromise,
|
|
496
|
+
type InitializedObservable,
|
|
497
|
+
map,
|
|
517
498
|
switchMap,
|
|
499
|
+
withInitialValue,
|
|
518
500
|
} from 'synstate';
|
|
501
|
+
import { useObservableValue } from 'synstate-react-hooks';
|
|
519
502
|
import { Result } from 'ts-data-forge';
|
|
520
503
|
|
|
521
504
|
const [searchState, setSearchState] = createState('');
|
|
522
505
|
|
|
523
|
-
// Advanced reactive pipeline
|
|
524
|
-
const searchResults$:
|
|
525
|
-
|
|
506
|
+
// Advanced reactive pipeline with debounce and filtering
|
|
507
|
+
const searchResults$: InitializedObservable<
|
|
508
|
+
readonly Readonly<{ id: string; name: string }>[]
|
|
526
509
|
> = searchState
|
|
527
|
-
.pipe(
|
|
510
|
+
.pipe(debounce(300))
|
|
528
511
|
.pipe(filter((query) => query.length > 2))
|
|
529
512
|
.pipe(
|
|
530
513
|
switchMap((query) =>
|
|
531
|
-
|
|
532
|
-
fetch(`/api/search?q=${query}
|
|
514
|
+
fromAbortablePromise((signal) =>
|
|
515
|
+
fetch(`/api/search?q=${query}`, { signal }).then(
|
|
533
516
|
(r) =>
|
|
534
517
|
r.json() as Promise<
|
|
535
518
|
readonly Readonly<{ id: string; name: string }>[]
|
|
@@ -537,24 +520,13 @@ const searchResults$: Observable<
|
|
|
537
520
|
),
|
|
538
521
|
),
|
|
539
522
|
),
|
|
540
|
-
)
|
|
523
|
+
)
|
|
524
|
+
.pipe(filter((res) => Result.isOk(res)))
|
|
525
|
+
.pipe(map((res) => Result.unwrapOk(res)))
|
|
526
|
+
.pipe(withInitialValue([]));
|
|
541
527
|
|
|
542
528
|
const SearchBox = (): React.JSX.Element => {
|
|
543
|
-
const
|
|
544
|
-
readonly Readonly<{ id: string; name: string }>[]
|
|
545
|
-
>([]);
|
|
546
|
-
|
|
547
|
-
React.useEffect(() => {
|
|
548
|
-
const sub = searchResults$.subscribe((result) => {
|
|
549
|
-
if (Result.isOk(result)) {
|
|
550
|
-
setResults(result.value);
|
|
551
|
-
}
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
return () => {
|
|
555
|
-
sub.unsubscribe();
|
|
556
|
-
};
|
|
557
|
-
}, []);
|
|
529
|
+
const searchResults = useObservableValue(searchResults$);
|
|
558
530
|
|
|
559
531
|
return (
|
|
560
532
|
<div>
|
|
@@ -565,7 +537,7 @@ const SearchBox = (): React.JSX.Element => {
|
|
|
565
537
|
}}
|
|
566
538
|
/>
|
|
567
539
|
<ul>
|
|
568
|
-
{
|
|
540
|
+
{searchResults.map((item) => (
|
|
569
541
|
<li key={item.id}>{item.name}</li>
|
|
570
542
|
))}
|
|
571
543
|
</ul>
|
|
@@ -577,10 +549,12 @@ const SearchBox = (): React.JSX.Element => {
|
|
|
577
549
|
### Advanced: Event Emitter with Throttle
|
|
578
550
|
|
|
579
551
|
```tsx
|
|
580
|
-
import { createEventEmitter,
|
|
552
|
+
import { createEventEmitter, throttle } from 'synstate';
|
|
581
553
|
|
|
582
554
|
// Create event emitter
|
|
583
555
|
const [refreshClicked, onRefreshClick] = createEventEmitter();
|
|
556
|
+
// refreshClicked: Observable<void>
|
|
557
|
+
// onRefreshClick: () => void
|
|
584
558
|
|
|
585
559
|
// Subscribe to events
|
|
586
560
|
refreshClicked.subscribe(() => {
|
|
@@ -588,7 +562,7 @@ refreshClicked.subscribe(() => {
|
|
|
588
562
|
});
|
|
589
563
|
|
|
590
564
|
// Throttle refresh clicks to prevent rapid successive executions
|
|
591
|
-
const throttledRefresh = refreshClicked.pipe(
|
|
565
|
+
const throttledRefresh = refreshClicked.pipe(throttle(2000));
|
|
592
566
|
|
|
593
567
|
throttledRefresh.subscribe(() => {
|
|
594
568
|
console.log('Executing refresh...');
|
|
@@ -607,37 +581,82 @@ const DataTable = (): React.JSX.Element => (
|
|
|
607
581
|
);
|
|
608
582
|
```
|
|
609
583
|
|
|
610
|
-
##
|
|
584
|
+
## API Reference
|
|
611
585
|
|
|
612
|
-
###
|
|
586
|
+
### State Management
|
|
613
587
|
|
|
614
|
-
SynState
|
|
588
|
+
SynState provides simple, intuitive APIs for managing application state:
|
|
615
589
|
|
|
616
|
-
|
|
590
|
+
- **`createState`**: Create state with InitializedObservable and setter
|
|
591
|
+
- **`createReducer`**: Create state by reducer and initial value
|
|
592
|
+
- **`createBooleanState`**: Specialized state for boolean values
|
|
617
593
|
|
|
618
|
-
|
|
594
|
+
### Event System
|
|
619
595
|
|
|
620
|
-
|
|
596
|
+
Built-in event emitter for event-driven patterns:
|
|
621
597
|
|
|
622
|
-
-
|
|
623
|
-
-
|
|
624
|
-
- **Better Readability**: No need for complex operator chains in everyday code
|
|
625
|
-
- **Optional Complexity**: Advanced features available when needed
|
|
598
|
+
- **`createValueEmitter`**: Create type-safe event emitters
|
|
599
|
+
- **`createEventEmitter`**: Create event emitters without payload
|
|
626
600
|
|
|
627
|
-
###
|
|
601
|
+
### Observable APIs
|
|
628
602
|
|
|
629
|
-
|
|
603
|
+
For complex scenarios, SynState provides observable-based APIs:
|
|
630
604
|
|
|
631
|
-
|
|
632
|
-
- ✅ Event-driven communication between components
|
|
633
|
-
- ✅ Type-safe event emitters
|
|
634
|
-
- ✅ Redux-like state with reducers
|
|
635
|
-
- ✅ Simple reactive patterns (debounce, throttle, etc.)
|
|
605
|
+
#### Creation Functions
|
|
636
606
|
|
|
637
|
-
|
|
607
|
+
- `source<T>()`: Create a new observable source (Almost equivalent to RxJS `subject`)
|
|
608
|
+
- `fromPromise(promise)`: Create observable from promise
|
|
609
|
+
- `fromSubscribable()`: Create observable from any subscribable object
|
|
610
|
+
- `counter(ms)`: Emit values at intervals (Almost equivalent to RxJS `interval`)
|
|
611
|
+
- `timer(delay)`: Emit after delay
|
|
612
|
+
|
|
613
|
+
#### Operators
|
|
614
|
+
|
|
615
|
+
- `map` variants
|
|
616
|
+
- `map(fn)`: Transform values
|
|
617
|
+
- `mapTo(value)`: Map all values to a constant
|
|
618
|
+
- `getKey(key)`: Extract property value from objects (alias: `pluck`)
|
|
619
|
+
- `attachIndex()`: Attach index to each value (alias: `withIndex`)
|
|
620
|
+
- Result/Optional
|
|
621
|
+
- `mapOptional(fn)`: Map over Optional values
|
|
622
|
+
- `mapResultOk(fn)`: Map over Result ok values
|
|
623
|
+
- `mapResultErr(fn)`: Map over Result error values
|
|
624
|
+
- `unwrapOptional()`: Unwrap Optional values to undefined
|
|
625
|
+
- `unwrapResultOk()`: Unwrap Result ok values to undefined
|
|
626
|
+
- `unwrapResultErr()`: Unwrap Result error values to undefined
|
|
627
|
+
- `mergeMap(fn)`: Map to observables and merge all (runs in parallel) (alias: `flatMap`)
|
|
628
|
+
- `switchMap(fn)`: Map to observables and switch to latest (cancels previous)
|
|
629
|
+
- Filtering
|
|
630
|
+
- `filter(predicate)`: Filter values
|
|
631
|
+
- `skipIfNoChange()`: Skip duplicate values (alias: `distinctUntilChanged`)
|
|
632
|
+
- `skip(n)`: Skip first n emissions
|
|
633
|
+
- `take(n)`: Take first n emissions then complete
|
|
634
|
+
- `skipWhile(predicate)`: Skip values while predicate is true
|
|
635
|
+
- `takeWhile(predicate)`: Emit values while predicate is true, then complete
|
|
636
|
+
- `skipUntil(notifier)`: Skip values until notifier emits
|
|
637
|
+
- `takeUntil(notifier)`: Complete on notifier emission
|
|
638
|
+
- Time series processing
|
|
639
|
+
- `audit(ms)`: Emit the last value after specified time window (Almost equivalent to RxJS `auditTime`)
|
|
640
|
+
- `debounce(ms)`: Debounce emissions (Almost equivalent to RxJS `debounceTime`)
|
|
641
|
+
- `throttle(ms)`: Throttle emissions (Almost equivalent to RxJS `throttleTime`)
|
|
642
|
+
- Others
|
|
643
|
+
- `pairwise()`: Emit previous and current values as pairs
|
|
644
|
+
- `scan(reducer, seed)`: Accumulate values
|
|
645
|
+
- `withBuffered(observable)`: Buffer values from observable and emit with parent (alias: `withBufferedFrom`)
|
|
646
|
+
- `withCurrentValueFrom(observable)`: Sample current value from another observable (alias: `withLatestFrom`)
|
|
647
|
+
- `withInitialValue(value)`: Provide an initial value for uninitialized observable
|
|
648
|
+
|
|
649
|
+
#### Combination
|
|
650
|
+
|
|
651
|
+
- `combine(observables)`: Combine latest values from multiple sources (alias: `combineLatest`)
|
|
652
|
+
- `merge(observables)`: Merge multiple streams
|
|
653
|
+
- `zip(observables)`: Pair values by index
|
|
654
|
+
|
|
655
|
+
#### Utilities
|
|
638
656
|
|
|
639
|
-
-
|
|
640
|
-
-
|
|
657
|
+
- `isChildObservable(obs)`: Check if observable is a child observable
|
|
658
|
+
- `isManagerObservable(obs)`: Check if observable is a manager observable
|
|
659
|
+
- `isRootObservable(obs)`: Check if observable is a root observable
|
|
641
660
|
|
|
642
661
|
## Type Safety
|
|
643
662
|
|