synstate 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +272 -532
- package/assets/synstate-icon.png +0 -0
- package/dist/core/combine/combine.d.mts +33 -3
- package/dist/core/combine/combine.d.mts.map +1 -1
- package/dist/core/combine/combine.mjs +34 -4
- package/dist/core/combine/combine.mjs.map +1 -1
- package/dist/core/combine/merge.d.mts +30 -4
- package/dist/core/combine/merge.d.mts.map +1 -1
- package/dist/core/combine/merge.mjs +30 -4
- package/dist/core/combine/merge.mjs.map +1 -1
- package/dist/core/combine/zip.d.mts +28 -3
- package/dist/core/combine/zip.d.mts.map +1 -1
- package/dist/core/combine/zip.mjs +28 -3
- package/dist/core/combine/zip.mjs.map +1 -1
- package/dist/core/create/from-array.d.mts +21 -3
- package/dist/core/create/from-array.d.mts.map +1 -1
- package/dist/core/create/from-array.mjs +21 -3
- package/dist/core/create/from-array.mjs.map +1 -1
- package/dist/core/create/from-promise.d.mts +29 -7
- package/dist/core/create/from-promise.d.mts.map +1 -1
- package/dist/core/create/from-promise.mjs +29 -7
- package/dist/core/create/from-promise.mjs.map +1 -1
- package/dist/core/create/from-subscribable.d.mts +58 -0
- package/dist/core/create/from-subscribable.d.mts.map +1 -1
- package/dist/core/create/from-subscribable.mjs +58 -0
- package/dist/core/create/from-subscribable.mjs.map +1 -1
- package/dist/core/create/interval.d.mts +29 -4
- package/dist/core/create/interval.d.mts.map +1 -1
- package/dist/core/create/interval.mjs +29 -4
- package/dist/core/create/interval.mjs.map +1 -1
- package/dist/core/create/of.d.mts +22 -3
- package/dist/core/create/of.d.mts.map +1 -1
- package/dist/core/create/of.mjs +22 -3
- package/dist/core/create/of.mjs.map +1 -1
- package/dist/core/create/source.d.mts +21 -2
- package/dist/core/create/source.d.mts.map +1 -1
- package/dist/core/create/source.mjs +2 -2
- package/dist/core/create/source.mjs.map +1 -1
- package/dist/core/create/timer.d.mts +23 -4
- package/dist/core/create/timer.d.mts.map +1 -1
- package/dist/core/create/timer.mjs +23 -4
- package/dist/core/create/timer.mjs.map +1 -1
- package/dist/core/index.d.mts +1 -0
- package/dist/core/index.d.mts.map +1 -1
- package/dist/core/index.mjs +15 -3
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/operators/audit-time.d.mts +59 -0
- package/dist/core/operators/audit-time.d.mts.map +1 -1
- package/dist/core/operators/audit-time.mjs +59 -0
- package/dist/core/operators/audit-time.mjs.map +1 -1
- package/dist/core/operators/debounce-time.d.mts +22 -2
- package/dist/core/operators/debounce-time.d.mts.map +1 -1
- package/dist/core/operators/debounce-time.mjs +22 -2
- package/dist/core/operators/debounce-time.mjs.map +1 -1
- package/dist/core/operators/filter.d.mts +26 -1
- package/dist/core/operators/filter.d.mts.map +1 -1
- package/dist/core/operators/filter.mjs.map +1 -1
- package/dist/core/operators/index.mjs +3 -3
- package/dist/core/operators/map-with-index.d.mts +19 -17
- package/dist/core/operators/map-with-index.d.mts.map +1 -1
- package/dist/core/operators/map-with-index.mjs +21 -23
- package/dist/core/operators/map-with-index.mjs.map +1 -1
- package/dist/core/operators/merge-map.d.mts +48 -6
- package/dist/core/operators/merge-map.d.mts.map +1 -1
- package/dist/core/operators/merge-map.mjs +48 -6
- package/dist/core/operators/merge-map.mjs.map +1 -1
- package/dist/core/operators/pairwise.d.mts +30 -1
- package/dist/core/operators/pairwise.d.mts.map +1 -1
- package/dist/core/operators/pairwise.mjs +30 -1
- package/dist/core/operators/pairwise.mjs.map +1 -1
- package/dist/core/operators/scan.d.mts +23 -1
- package/dist/core/operators/scan.d.mts.map +1 -1
- package/dist/core/operators/scan.mjs +23 -1
- package/dist/core/operators/scan.mjs.map +1 -1
- package/dist/core/operators/skip-if-no-change.d.mts +26 -2
- package/dist/core/operators/skip-if-no-change.d.mts.map +1 -1
- package/dist/core/operators/skip-if-no-change.mjs +27 -3
- package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
- package/dist/core/operators/skip-until.d.mts +50 -0
- package/dist/core/operators/skip-until.d.mts.map +1 -1
- package/dist/core/operators/skip-until.mjs +50 -0
- package/dist/core/operators/skip-until.mjs.map +1 -1
- package/dist/core/operators/skip-while.d.mts +48 -1
- package/dist/core/operators/skip-while.d.mts.map +1 -1
- package/dist/core/operators/skip-while.mjs +50 -5
- package/dist/core/operators/skip-while.mjs.map +1 -1
- package/dist/core/operators/switch-map.d.mts +39 -5
- package/dist/core/operators/switch-map.d.mts.map +1 -1
- package/dist/core/operators/switch-map.mjs +39 -5
- package/dist/core/operators/switch-map.mjs.map +1 -1
- package/dist/core/operators/take-until.d.mts +20 -1
- package/dist/core/operators/take-until.d.mts.map +1 -1
- package/dist/core/operators/take-until.mjs +20 -1
- package/dist/core/operators/take-until.mjs.map +1 -1
- package/dist/core/operators/take-while.d.mts +47 -1
- package/dist/core/operators/take-while.d.mts.map +1 -1
- package/dist/core/operators/take-while.mjs +48 -3
- package/dist/core/operators/take-while.mjs.map +1 -1
- package/dist/core/operators/throttle-time.d.mts +44 -5
- package/dist/core/operators/throttle-time.d.mts.map +1 -1
- package/dist/core/operators/throttle-time.mjs +44 -5
- package/dist/core/operators/throttle-time.mjs.map +1 -1
- package/dist/core/operators/with-buffered-from.d.mts +57 -0
- package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
- package/dist/core/operators/with-buffered-from.mjs +58 -1
- package/dist/core/operators/with-buffered-from.mjs.map +1 -1
- package/dist/core/operators/with-current-value-from.d.mts +59 -0
- package/dist/core/operators/with-current-value-from.d.mts.map +1 -1
- package/dist/core/operators/with-current-value-from.mjs +60 -1
- package/dist/core/operators/with-current-value-from.mjs.map +1 -1
- package/dist/core/operators/with-initial-value.d.mts +24 -2
- package/dist/core/operators/with-initial-value.d.mts.map +1 -1
- package/dist/core/operators/with-initial-value.mjs +24 -2
- 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 +13 -0
- package/dist/core/predefined/index.mjs.map +1 -0
- package/dist/core/predefined/operators/attach-index.d.mts +8 -0
- package/dist/core/predefined/operators/attach-index.d.mts.map +1 -0
- package/dist/core/predefined/operators/attach-index.mjs +13 -0
- package/dist/core/predefined/operators/attach-index.mjs.map +1 -0
- package/dist/core/predefined/operators/index.d.mts +13 -0
- package/dist/core/predefined/operators/index.d.mts.map +1 -0
- package/dist/core/predefined/operators/index.mjs +13 -0
- package/dist/core/predefined/operators/index.mjs.map +1 -0
- package/dist/core/predefined/operators/map-optional.d.mts +4 -0
- package/dist/core/predefined/operators/map-optional.d.mts.map +1 -0
- package/dist/core/predefined/operators/map-optional.mjs +7 -0
- package/dist/core/predefined/operators/map-optional.mjs.map +1 -0
- package/dist/core/predefined/operators/map-result-err.d.mts +4 -0
- package/dist/core/predefined/operators/map-result-err.d.mts.map +1 -0
- package/dist/core/predefined/operators/map-result-err.mjs +7 -0
- package/dist/core/predefined/operators/map-result-err.mjs.map +1 -0
- package/dist/core/predefined/operators/map-result-ok.d.mts +4 -0
- package/dist/core/predefined/operators/map-result-ok.d.mts.map +1 -0
- package/dist/core/predefined/operators/map-result-ok.mjs +7 -0
- package/dist/core/predefined/operators/map-result-ok.mjs.map +1 -0
- package/dist/core/predefined/operators/map-to.d.mts +3 -0
- package/dist/core/predefined/operators/map-to.d.mts.map +1 -0
- package/dist/core/predefined/operators/map-to.mjs +6 -0
- package/dist/core/predefined/operators/map-to.mjs.map +1 -0
- package/dist/core/predefined/operators/map.d.mts +3 -0
- package/dist/core/predefined/operators/map.d.mts.map +1 -0
- package/dist/core/predefined/operators/map.mjs +8 -0
- package/dist/core/predefined/operators/map.mjs.map +1 -0
- package/dist/core/predefined/operators/pluck.d.mts +8 -0
- package/dist/core/predefined/operators/pluck.d.mts.map +1 -0
- package/dist/core/predefined/operators/pluck.mjs +11 -0
- package/dist/core/predefined/operators/pluck.mjs.map +1 -0
- package/dist/core/predefined/operators/skip.d.mts +3 -0
- package/dist/core/predefined/operators/skip.d.mts.map +1 -0
- package/dist/core/predefined/operators/skip.mjs +9 -0
- package/dist/core/predefined/operators/skip.mjs.map +1 -0
- package/dist/core/predefined/operators/take.d.mts +3 -0
- package/dist/core/predefined/operators/take.d.mts.map +1 -0
- package/dist/core/predefined/operators/take.mjs +8 -0
- package/dist/core/predefined/operators/take.mjs.map +1 -0
- package/dist/core/predefined/operators/unwrap-optional.d.mts +4 -0
- package/dist/core/predefined/operators/unwrap-optional.d.mts.map +1 -0
- package/dist/core/predefined/operators/unwrap-optional.mjs +9 -0
- package/dist/core/predefined/operators/unwrap-optional.mjs.map +1 -0
- package/dist/core/predefined/operators/unwrap-result-err.d.mts +4 -0
- package/dist/core/predefined/operators/unwrap-result-err.d.mts.map +1 -0
- package/dist/core/predefined/operators/unwrap-result-err.mjs +7 -0
- package/dist/core/predefined/operators/unwrap-result-err.mjs.map +1 -0
- package/dist/core/predefined/operators/unwrap-result-ok.d.mts +4 -0
- package/dist/core/predefined/operators/unwrap-result-ok.d.mts.map +1 -0
- package/dist/core/predefined/operators/unwrap-result-ok.mjs +9 -0
- package/dist/core/predefined/operators/unwrap-result-ok.mjs.map +1 -0
- package/dist/core/types/observable-family.d.mts +7 -7
- package/dist/entry-point.mjs +15 -3
- package/dist/entry-point.mjs.map +1 -1
- package/dist/index.mjs +15 -3
- package/dist/index.mjs.map +1 -1
- package/dist/utils/create-event-emitter.d.mts +20 -2
- package/dist/utils/create-event-emitter.d.mts.map +1 -1
- package/dist/utils/create-event-emitter.mjs +20 -2
- package/dist/utils/create-event-emitter.mjs.map +1 -1
- package/dist/utils/create-reducer.d.mts +13 -1
- package/dist/utils/create-reducer.d.mts.map +1 -1
- package/dist/utils/create-reducer.mjs +13 -1
- package/dist/utils/create-reducer.mjs.map +1 -1
- package/dist/utils/create-state.d.mts +24 -4
- package/dist/utils/create-state.d.mts.map +1 -1
- package/dist/utils/create-state.mjs +24 -4
- package/dist/utils/create-state.mjs.map +1 -1
- package/package.json +13 -12
- package/src/core/combine/combine.mts +34 -4
- package/src/core/combine/merge.mts +30 -4
- package/src/core/combine/zip.mts +28 -3
- package/src/core/create/from-array.mts +21 -3
- package/src/core/create/from-promise.mts +29 -7
- package/src/core/create/from-subscribable.mts +58 -0
- package/src/core/create/interval.mts +29 -4
- package/src/core/create/of.mts +22 -3
- package/src/core/create/source.mts +22 -3
- package/src/core/create/timer.mts +23 -4
- package/src/core/index.mts +1 -0
- package/src/core/operators/audit-time.mts +59 -0
- package/src/core/operators/debounce-time.mts +22 -2
- package/src/core/operators/filter.mts +26 -1
- package/src/core/operators/map-with-index.mts +22 -66
- package/src/core/operators/merge-map.mts +48 -6
- package/src/core/operators/pairwise.mts +30 -1
- package/src/core/operators/scan.mts +23 -1
- package/src/core/operators/skip-if-no-change.mts +27 -3
- package/src/core/operators/skip-until.mts +50 -0
- package/src/core/operators/skip-while.mts +49 -16
- package/src/core/operators/switch-map.mts +39 -5
- package/src/core/operators/take-until.mts +20 -1
- package/src/core/operators/take-while.mts +49 -8
- package/src/core/operators/throttle-time.mts +44 -5
- package/src/core/operators/with-buffered-from.mts +58 -1
- package/src/core/operators/with-current-value-from.mts +60 -1
- package/src/core/operators/with-initial-value.mts +24 -2
- package/src/core/predefined/index.mts +1 -0
- package/src/core/predefined/operators/attach-index.mts +13 -0
- package/src/core/predefined/operators/index.mts +12 -0
- package/src/core/predefined/operators/map-optional.mts +8 -0
- package/src/core/predefined/operators/map-result-err.mts +8 -0
- package/src/core/predefined/operators/map-result-ok.mts +8 -0
- package/src/core/predefined/operators/map-to.mts +5 -0
- package/src/core/predefined/operators/map.mts +5 -0
- package/src/core/predefined/operators/pluck.mts +12 -0
- package/src/core/predefined/operators/skip.mts +10 -0
- package/src/core/predefined/operators/take.mts +6 -0
- package/src/core/predefined/operators/unwrap-optional.mts +9 -0
- package/src/core/predefined/operators/unwrap-result-err.mts +8 -0
- package/src/core/predefined/operators/unwrap-result-ok.mts +9 -0
- package/src/core/types/observable-family.mts +7 -7
- package/src/utils/create-event-emitter.mts +20 -2
- package/src/utils/create-reducer.mts +13 -1
- package/src/utils/create-state.mts +24 -4
package/README.md
CHANGED
|
@@ -1,25 +1,34 @@
|
|
|
1
|
-
#
|
|
1
|
+
# SynState
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
[](https://codecov.io/gh/noshiro-pf/ts-data-forge)
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="./assets/synstate-icon.png" alt="SynState Logo" width="400" />
|
|
5
|
+
</p>
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/synstate)
|
|
10
|
+
[](https://www.npmjs.com/package/synstate)
|
|
11
|
+
[](./LICENSE)
|
|
12
|
+
[](https://codecov.io/gh/noshiro-pf/synstate)
|
|
13
|
+
|
|
14
|
+
</p>
|
|
15
|
+
|
|
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.
|
|
9
17
|
|
|
10
18
|
## Features
|
|
11
19
|
|
|
12
20
|
- 🎯 **Simple State Management**: Easy-to-use `createState` and `createReducer` for global state
|
|
13
21
|
- 📡 **Event System**: Built-in `createValueEmitter`, `createEventEmitter` for event-driven architecture
|
|
14
|
-
- 🔄 **Reactive Updates**: Automatic propagation of state changes to subscribers
|
|
22
|
+
- 🔄 **Reactive Updates**: Automatic propagation of state changes to all subscribers
|
|
15
23
|
- 🎨 **Type-Safe**: Full TypeScript support with precise type inference
|
|
16
|
-
- 🚀 **Lightweight**: Minimal bundle size
|
|
17
|
-
- ⚡ **
|
|
18
|
-
-
|
|
24
|
+
- 🚀 **Lightweight**: Minimal bundle size with only one external runtime dependency ([ts-data-forge](https://www.npmjs.com/package/ts-data-forge))
|
|
25
|
+
- ⚡ **High Performance**: Optimized for fast state updates and minimal re-renders
|
|
26
|
+
- 🌐 **Framework Agnostic**: Works with React, Vue, Svelte, or vanilla JavaScript
|
|
27
|
+
- 🔧 **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`, `debounceTime`) and combinators (`merge`, `combine`)
|
|
19
28
|
|
|
20
29
|
## Documentation
|
|
21
30
|
|
|
22
|
-
- API reference: <https://noshiro-pf.github.io/synstate/>
|
|
31
|
+
- API reference: TBD <!-- <https://noshiro-pf.github.io/synstate/> -->
|
|
23
32
|
|
|
24
33
|
## Installation
|
|
25
34
|
|
|
@@ -42,39 +51,33 @@ pnpm add synstate
|
|
|
42
51
|
### Simple State Management
|
|
43
52
|
|
|
44
53
|
```tsx
|
|
45
|
-
import { createState } from 'synstate';
|
|
46
|
-
|
|
47
54
|
// Create a reactive state
|
|
48
|
-
const [state, setState, { updateState }] =
|
|
55
|
+
const [state, setState, { updateState, resetState, getSnapshot }] =
|
|
56
|
+
createState(0);
|
|
57
|
+
|
|
58
|
+
const mut_history: number[] = [];
|
|
49
59
|
|
|
50
60
|
// Subscribe to changes (in React components, Vue watchers, etc.)
|
|
51
|
-
state.subscribe((count
|
|
52
|
-
|
|
61
|
+
state.subscribe((count) => {
|
|
62
|
+
mut_history.push(count);
|
|
53
63
|
});
|
|
54
64
|
|
|
65
|
+
assert.deepStrictEqual(mut_history, [0]);
|
|
66
|
+
|
|
55
67
|
// Update state
|
|
56
68
|
setState(1);
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
```
|
|
70
|
+
assert.deepStrictEqual(mut_history, [0, 1]);
|
|
60
71
|
|
|
61
|
-
|
|
72
|
+
updateState((prev) => prev + 2);
|
|
62
73
|
|
|
63
|
-
|
|
64
|
-
import { createValueEmitter } from 'synstate';
|
|
74
|
+
assert.deepStrictEqual(mut_history, [0, 1, 3]);
|
|
65
75
|
|
|
66
|
-
|
|
76
|
+
assert.isTrue(getSnapshot() === 3);
|
|
67
77
|
|
|
68
|
-
|
|
69
|
-
const [userLoggedIn$, emitUserLoggedIn] = createValueEmitter<User>();
|
|
78
|
+
resetState();
|
|
70
79
|
|
|
71
|
-
|
|
72
|
-
userLoggedIn$.subscribe((user) => {
|
|
73
|
-
console.log('User logged in:', user.name);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// Emit events
|
|
77
|
-
emitUserLoggedIn({ id: 1, name: 'Alice' });
|
|
80
|
+
assert.isTrue(getSnapshot() === 0);
|
|
78
81
|
```
|
|
79
82
|
|
|
80
83
|
### With React
|
|
@@ -121,14 +124,97 @@ const UserProfile = (): React.JSX.Element => {
|
|
|
121
124
|
};
|
|
122
125
|
```
|
|
123
126
|
|
|
127
|
+
If you're using React v18 or later:
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
import * as React from 'react';
|
|
131
|
+
import { createState } from 'synstate';
|
|
132
|
+
|
|
133
|
+
const [userState, setUserState] = createState({
|
|
134
|
+
name: '',
|
|
135
|
+
email: '',
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const UserProfile = (): React.JSX.Element => {
|
|
139
|
+
const user = React.useSyncExternalStore(
|
|
140
|
+
(onStoreChange: () => void) => {
|
|
141
|
+
const { unsubscribe } = userState.subscribe(onStoreChange);
|
|
142
|
+
|
|
143
|
+
return unsubscribe;
|
|
144
|
+
},
|
|
145
|
+
() => userState.getSnapshot().value,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div>
|
|
150
|
+
<p>
|
|
151
|
+
{'Name: '}
|
|
152
|
+
{user.name}
|
|
153
|
+
</p>
|
|
154
|
+
<button
|
|
155
|
+
onClick={() => {
|
|
156
|
+
setUserState({
|
|
157
|
+
name: 'Alice',
|
|
158
|
+
email: 'alice@example.com',
|
|
159
|
+
});
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
{'Set User'}
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
You can write the equivalent code more concisely using synstate-react-hooks:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
npm add synstate-react-hooks
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
import type * as React from 'react';
|
|
177
|
+
import { createState } from 'synstate-react-hooks';
|
|
178
|
+
|
|
179
|
+
const [useUserState, setUserState] = createState({
|
|
180
|
+
name: '',
|
|
181
|
+
email: '',
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const UserProfile = (): React.JSX.Element => {
|
|
185
|
+
const user = useUserState();
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<div>
|
|
189
|
+
<p>
|
|
190
|
+
{'Name: '}
|
|
191
|
+
{user.name}
|
|
192
|
+
</p>
|
|
193
|
+
<button
|
|
194
|
+
onClick={() => {
|
|
195
|
+
setUserState({
|
|
196
|
+
name: 'Alice',
|
|
197
|
+
email: 'alice@example.com',
|
|
198
|
+
});
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
{'Set User'}
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
};
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
See also the [synstate-react-hooks README](../synstate-react-hooks/README.md).
|
|
209
|
+
|
|
124
210
|
## Core Concepts
|
|
125
211
|
|
|
126
212
|
### State Management
|
|
127
213
|
|
|
128
|
-
|
|
214
|
+
SynState provides simple, intuitive APIs for managing application state:
|
|
129
215
|
|
|
130
|
-
- **`createState`**: Create
|
|
131
|
-
- **`createReducer`**:
|
|
216
|
+
- **`createState`**: Create state with getter/setter
|
|
217
|
+
- **`createReducer`**: Create state by reducer and initial value
|
|
132
218
|
- **`createBooleanState`**: Specialized state for boolean values
|
|
133
219
|
|
|
134
220
|
### Event System
|
|
@@ -142,25 +228,50 @@ Built-in event emitter for event-driven patterns:
|
|
|
142
228
|
|
|
143
229
|
For advanced use cases, you can use observables to build complex reactive data flows. However, most applications will only need `createState`, `createReducer`, and `createValueEmitter`.
|
|
144
230
|
|
|
231
|
+
## API Reference
|
|
232
|
+
|
|
233
|
+
For complex scenarios, SynState provides observable-based APIs:
|
|
234
|
+
|
|
235
|
+
### Creation Functions
|
|
236
|
+
|
|
237
|
+
- `source<T>()`: Create a new observable source
|
|
238
|
+
- `of(value)`: Create observable from a single value
|
|
239
|
+
- `fromArray(array)`: Create observable from array
|
|
240
|
+
- `fromPromise(promise)`: Create observable from promise
|
|
241
|
+
- `interval(ms)`: Emit values at intervals
|
|
242
|
+
- `timer(delay)`: Emit after delay
|
|
243
|
+
|
|
244
|
+
### Operators
|
|
245
|
+
|
|
246
|
+
- `filter(predicate)`: Filter values
|
|
247
|
+
- `map(fn)`: Transform values
|
|
248
|
+
- `scan(reducer, seed)`: Accumulate values
|
|
249
|
+
- `debounceTime(ms)`: Debounce emissions
|
|
250
|
+
- `throttleTime(ms)`: Throttle emissions
|
|
251
|
+
- `skipIfNoChange()`: Skip duplicate values
|
|
252
|
+
- `takeUntil(notifier)`: Complete on notifier emission
|
|
253
|
+
|
|
254
|
+
### Combination
|
|
255
|
+
|
|
256
|
+
- `combine(observables)`: Combine latest values from multiple sources
|
|
257
|
+
- `merge(observables)`: Merge multiple streams
|
|
258
|
+
- `zip(observables)`: Pair values by index
|
|
259
|
+
|
|
260
|
+
## Examples
|
|
261
|
+
|
|
262
|
+
### Global Counter State (React)
|
|
263
|
+
|
|
145
264
|
```tsx
|
|
146
|
-
import * as React from 'react';
|
|
147
|
-
import { createState } from 'synstate';
|
|
265
|
+
import type * as React from 'react';
|
|
266
|
+
import { createState } from 'synstate-react-hooks';
|
|
148
267
|
|
|
149
268
|
// Create global state
|
|
150
|
-
export const [
|
|
269
|
+
export const [useCounterState, , { updateState, resetState, getSnapshot }] =
|
|
151
270
|
createState(0);
|
|
152
271
|
|
|
153
272
|
// Component 1
|
|
154
273
|
const Counter = (): React.JSX.Element => {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
React.useEffect(() => {
|
|
158
|
-
const sub = counterState.subscribe(setCount);
|
|
159
|
-
|
|
160
|
-
return () => {
|
|
161
|
-
sub.unsubscribe();
|
|
162
|
-
};
|
|
163
|
-
}, []);
|
|
274
|
+
const count = useCounterState();
|
|
164
275
|
|
|
165
276
|
return (
|
|
166
277
|
<div>
|
|
@@ -191,85 +302,17 @@ const ResetButton = (): React.JSX.Element => (
|
|
|
191
302
|
);
|
|
192
303
|
```
|
|
193
304
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
### State Management (Recommended)
|
|
197
|
-
|
|
198
|
-
#### createState
|
|
199
|
-
|
|
200
|
-
Create reactive state with getter and setter:
|
|
201
|
-
|
|
202
|
-
```tsx
|
|
203
|
-
import * as React from 'react';
|
|
204
|
-
import { createEventEmitter, createValueEmitter } from 'synstate';
|
|
205
|
-
|
|
206
|
-
// Global events
|
|
207
|
-
export const [userLoggedIn$, emitUserLoggedIn] = createValueEmitter<
|
|
208
|
-
Readonly<{
|
|
209
|
-
id: number;
|
|
210
|
-
name: string;
|
|
211
|
-
}>
|
|
212
|
-
>();
|
|
213
|
-
|
|
214
|
-
export const [userLoggedOut$, emitUserLoggedOut] = createEventEmitter();
|
|
215
|
-
|
|
216
|
-
// Component that emits events
|
|
217
|
-
const LoginButton = (): React.JSX.Element => {
|
|
218
|
-
const handleLogin = React.useCallback(() => {
|
|
219
|
-
(async () => {
|
|
220
|
-
const user = await loginUser();
|
|
221
|
-
|
|
222
|
-
emitUserLoggedIn(user);
|
|
223
|
-
})().catch(() => {});
|
|
224
|
-
}, []);
|
|
225
|
-
|
|
226
|
-
return <button onClick={handleLogin}>{'Login'}</button>;
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
// Component that listens to events
|
|
230
|
-
const NotificationPage = (): React.JSX.Element => {
|
|
231
|
-
const [message, setMessage] = React.useState('');
|
|
232
|
-
|
|
233
|
-
React.useEffect(() => {
|
|
234
|
-
const sub1 = userLoggedIn$.subscribe((user) => {
|
|
235
|
-
setMessage(`Welcome, ${user.name}!`);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
const sub2 = userLoggedOut$.subscribe(() => {
|
|
239
|
-
setMessage('Logged out');
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
return () => {
|
|
243
|
-
sub1.unsubscribe();
|
|
244
|
-
|
|
245
|
-
sub2.unsubscribe();
|
|
246
|
-
};
|
|
247
|
-
}, []);
|
|
248
|
-
|
|
249
|
-
return message !== '' ? (
|
|
250
|
-
<div className={'notification'}>{message}</div>
|
|
251
|
-
) : (
|
|
252
|
-
<>{null}</>
|
|
253
|
-
);
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
const loginUser = async (): Promise<
|
|
257
|
-
Readonly<{
|
|
258
|
-
id: number;
|
|
259
|
-
name: string;
|
|
260
|
-
}>
|
|
261
|
-
> => ({ id: 1, name: 'Alice' });
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
#### createBooleanState
|
|
265
|
-
|
|
266
|
-
Specialized state for boolean values:
|
|
305
|
+
### Todo List with Reducer (React)
|
|
267
306
|
|
|
268
307
|
```tsx
|
|
269
308
|
import * as React from 'react';
|
|
270
|
-
import { createReducer } from 'synstate';
|
|
309
|
+
import { createReducer } from 'synstate-react-hooks';
|
|
271
310
|
|
|
272
|
-
type Todo = Readonly<{
|
|
311
|
+
type Todo = Readonly<{
|
|
312
|
+
id: number;
|
|
313
|
+
text: string;
|
|
314
|
+
done: boolean;
|
|
315
|
+
}>;
|
|
273
316
|
|
|
274
317
|
type Action = Readonly<
|
|
275
318
|
| { type: 'add'; text: string }
|
|
@@ -277,10 +320,9 @@ type Action = Readonly<
|
|
|
277
320
|
| { type: 'remove'; id: number }
|
|
278
321
|
>;
|
|
279
322
|
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
>((todos, action) => {
|
|
323
|
+
const initialTodos: readonly Todo[] = [];
|
|
324
|
+
|
|
325
|
+
const reducer = (todos: readonly Todo[], action: Action): readonly Todo[] => {
|
|
284
326
|
switch (action.type) {
|
|
285
327
|
case 'add':
|
|
286
328
|
return [
|
|
@@ -291,128 +333,109 @@ const [todoState, dispatch, getSnapshot] = createReducer<
|
|
|
291
333
|
done: false,
|
|
292
334
|
},
|
|
293
335
|
];
|
|
336
|
+
|
|
294
337
|
case 'toggle':
|
|
295
338
|
return todos.map((t) =>
|
|
296
339
|
t.id === action.id ? { ...t, done: !t.done } : t,
|
|
297
340
|
);
|
|
341
|
+
|
|
298
342
|
case 'remove':
|
|
299
343
|
return todos.filter((t) => t.id !== action.id);
|
|
300
344
|
}
|
|
301
|
-
}
|
|
345
|
+
};
|
|
302
346
|
|
|
303
|
-
const
|
|
304
|
-
|
|
347
|
+
const [useTodoState, dispatch] = createReducer<readonly Todo[], Action>(
|
|
348
|
+
reducer,
|
|
349
|
+
initialTodos,
|
|
350
|
+
);
|
|
305
351
|
|
|
306
|
-
|
|
307
|
-
|
|
352
|
+
const addTodo = (): void => {
|
|
353
|
+
dispatch({
|
|
354
|
+
type: 'add',
|
|
355
|
+
text: 'New Todo',
|
|
356
|
+
});
|
|
357
|
+
};
|
|
308
358
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
359
|
+
const TodoList = (): React.JSX.Element => {
|
|
360
|
+
const todos = useTodoState();
|
|
361
|
+
|
|
362
|
+
const todosWithHandler = React.useMemo(
|
|
363
|
+
() =>
|
|
364
|
+
todos.map((todo) => ({
|
|
365
|
+
...todo,
|
|
366
|
+
onToggle: () => {
|
|
367
|
+
dispatch({
|
|
368
|
+
type: 'toggle',
|
|
369
|
+
id: todo.id,
|
|
370
|
+
});
|
|
371
|
+
},
|
|
372
|
+
onRemove: () => {
|
|
373
|
+
dispatch({
|
|
374
|
+
type: 'remove',
|
|
375
|
+
id: todo.id,
|
|
376
|
+
});
|
|
377
|
+
},
|
|
378
|
+
})),
|
|
379
|
+
[todos],
|
|
380
|
+
);
|
|
313
381
|
|
|
314
382
|
return (
|
|
315
383
|
<div>
|
|
316
|
-
{
|
|
384
|
+
{todosWithHandler.map((todo) => (
|
|
317
385
|
<div key={todo.id}>
|
|
318
386
|
<input
|
|
319
387
|
checked={todo.done}
|
|
320
388
|
type={'checkbox'}
|
|
321
|
-
onChange={
|
|
322
|
-
dispatch({
|
|
323
|
-
type: 'toggle',
|
|
324
|
-
id: todo.id,
|
|
325
|
-
});
|
|
326
|
-
}}
|
|
389
|
+
onChange={todo.onToggle}
|
|
327
390
|
/>
|
|
328
391
|
<span>{todo.text}</span>
|
|
392
|
+
<button onClick={todo.onRemove}>{'Remove'}</button>
|
|
329
393
|
</div>
|
|
330
394
|
))}
|
|
331
|
-
<button
|
|
332
|
-
onClick={() => {
|
|
333
|
-
dispatch({
|
|
334
|
-
type: 'add',
|
|
335
|
-
text: 'New Todo',
|
|
336
|
-
});
|
|
337
|
-
}}
|
|
338
|
-
>
|
|
339
|
-
{'Add Todo'}
|
|
340
|
-
</button>
|
|
395
|
+
<button onClick={addTodo}>{'Add Todo'}</button>
|
|
341
396
|
</div>
|
|
342
397
|
);
|
|
343
398
|
};
|
|
344
399
|
```
|
|
345
400
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
Create state with reducer pattern (like Redux):
|
|
401
|
+
### Boolean State (Dark Mode)
|
|
349
402
|
|
|
350
403
|
```tsx
|
|
351
404
|
import * as React from 'react';
|
|
352
|
-
import { createBooleanState } from 'synstate';
|
|
405
|
+
import { createBooleanState } from 'synstate-react-hooks';
|
|
353
406
|
|
|
354
|
-
export const [
|
|
407
|
+
export const [useDarkModeState, { toggle: toggleDarkMode }] =
|
|
355
408
|
createBooleanState(false);
|
|
356
409
|
|
|
357
410
|
const ThemeToggle = (): React.JSX.Element => {
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
React.useEffect(() => {
|
|
361
|
-
const sub = darkModeState.subscribe(setIsDark);
|
|
362
|
-
|
|
363
|
-
return () => {
|
|
364
|
-
sub.unsubscribe();
|
|
365
|
-
};
|
|
366
|
-
}, []);
|
|
411
|
+
const isDark = useDarkModeState();
|
|
367
412
|
|
|
368
413
|
React.useEffect(() => {
|
|
369
414
|
document.body.className = isDark ? 'dark' : 'light';
|
|
370
415
|
}, [isDark]);
|
|
371
416
|
|
|
372
|
-
return
|
|
373
|
-
<button
|
|
374
|
-
onClick={() => {
|
|
375
|
-
toggle();
|
|
376
|
-
}}
|
|
377
|
-
>
|
|
378
|
-
{isDark ? '🌙' : '☀️'}
|
|
379
|
-
</button>
|
|
380
|
-
);
|
|
417
|
+
return <button onClick={toggleDarkMode}>{isDark ? '🌙' : '☀️'}</button>;
|
|
381
418
|
};
|
|
382
419
|
```
|
|
383
420
|
|
|
384
|
-
###
|
|
385
|
-
|
|
386
|
-
#### createValueEmitter
|
|
387
|
-
|
|
388
|
-
Create type-safe event emitter with payload:
|
|
421
|
+
### Cross-Component Communication
|
|
389
422
|
|
|
390
423
|
```tsx
|
|
391
424
|
import * as React from 'react';
|
|
392
|
-
import {
|
|
393
|
-
|
|
394
|
-
// Events
|
|
395
|
-
const [onItemAdded$, emitItemAdded] = createValueEmitter<string>();
|
|
396
|
-
|
|
397
|
-
const [onClearAll$, emitClearAll] = createEventEmitter();
|
|
425
|
+
import { createState } from 'synstate-react-hooks';
|
|
398
426
|
|
|
399
427
|
// State
|
|
400
|
-
const [
|
|
401
|
-
readonly string[]
|
|
402
|
-
>([]);
|
|
428
|
+
const [useItemsState, _, { updateState, resetState: resetItemsState }] =
|
|
429
|
+
createState<readonly string[]>([]);
|
|
403
430
|
|
|
404
431
|
// Setup event handlers
|
|
405
|
-
|
|
432
|
+
const addItem = (item: string): void => {
|
|
406
433
|
updateState((items: readonly string[]) => [...items, item]);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
onClearAll$.subscribe(() => {
|
|
410
|
-
setItemsState([]);
|
|
411
|
-
});
|
|
434
|
+
};
|
|
412
435
|
|
|
413
436
|
// Component 1: Add items
|
|
414
437
|
const ItemInput = (): React.JSX.Element => {
|
|
415
|
-
const [input, setInput] = React.useState('');
|
|
438
|
+
const [input, setInput] = React.useState<string>('');
|
|
416
439
|
|
|
417
440
|
return (
|
|
418
441
|
<div>
|
|
@@ -424,7 +447,7 @@ const ItemInput = (): React.JSX.Element => {
|
|
|
424
447
|
/>
|
|
425
448
|
<button
|
|
426
449
|
onClick={() => {
|
|
427
|
-
|
|
450
|
+
addItem(input);
|
|
428
451
|
|
|
429
452
|
setInput('');
|
|
430
453
|
}}
|
|
@@ -437,15 +460,7 @@ const ItemInput = (): React.JSX.Element => {
|
|
|
437
460
|
|
|
438
461
|
// Component 2: Display items
|
|
439
462
|
const ItemList = (): React.JSX.Element => {
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
React.useEffect(() => {
|
|
443
|
-
const sub = itemsState.subscribe(setItems);
|
|
444
|
-
|
|
445
|
-
return () => {
|
|
446
|
-
sub.unsubscribe();
|
|
447
|
-
};
|
|
448
|
-
}, []);
|
|
463
|
+
const items = useItemsState();
|
|
449
464
|
|
|
450
465
|
return (
|
|
451
466
|
<div>
|
|
@@ -454,33 +469,36 @@ const ItemList = (): React.JSX.Element => {
|
|
|
454
469
|
<li key={i}>{item}</li>
|
|
455
470
|
))}
|
|
456
471
|
</ul>
|
|
457
|
-
<button onClick={
|
|
472
|
+
<button onClick={resetItemsState}>{'Clear All'}</button>
|
|
458
473
|
</div>
|
|
459
474
|
);
|
|
460
475
|
};
|
|
461
476
|
```
|
|
462
477
|
|
|
463
|
-
|
|
478
|
+
// Events
|
|
464
479
|
|
|
465
|
-
|
|
480
|
+
### Advanced: Search with Debounce
|
|
466
481
|
|
|
467
482
|
```tsx
|
|
468
|
-
import * as React from 'react';
|
|
483
|
+
import type * as React from 'react';
|
|
469
484
|
import {
|
|
470
485
|
createState,
|
|
471
486
|
debounceTime,
|
|
472
487
|
filter,
|
|
473
488
|
fromPromise,
|
|
474
|
-
type
|
|
489
|
+
type InitializedObservable,
|
|
490
|
+
map,
|
|
475
491
|
switchMap,
|
|
492
|
+
withInitialValue,
|
|
476
493
|
} from 'synstate';
|
|
494
|
+
import { useObservableValue } from 'synstate-react-hooks';
|
|
477
495
|
import { Result } from 'ts-data-forge';
|
|
478
496
|
|
|
479
497
|
const [searchState, setSearchState] = createState('');
|
|
480
498
|
|
|
481
|
-
// Advanced reactive pipeline
|
|
482
|
-
const searchResults$:
|
|
483
|
-
|
|
499
|
+
// Advanced reactive pipeline with debounce and filtering
|
|
500
|
+
const searchResults$: InitializedObservable<
|
|
501
|
+
readonly Readonly<{ id: string; name: string }>[]
|
|
484
502
|
> = searchState
|
|
485
503
|
.pipe(debounceTime(300))
|
|
486
504
|
.pipe(filter((query) => query.length > 2))
|
|
@@ -495,24 +513,13 @@ const searchResults$: Observable<
|
|
|
495
513
|
),
|
|
496
514
|
),
|
|
497
515
|
),
|
|
498
|
-
)
|
|
516
|
+
)
|
|
517
|
+
.pipe(filter((res) => Result.isOk(res)))
|
|
518
|
+
.pipe(map((res) => Result.unwrapOk(res)))
|
|
519
|
+
.pipe(withInitialValue([]));
|
|
499
520
|
|
|
500
521
|
const SearchBox = (): React.JSX.Element => {
|
|
501
|
-
const
|
|
502
|
-
readonly Readonly<{ id: string; name: string }>[]
|
|
503
|
-
>([]);
|
|
504
|
-
|
|
505
|
-
React.useEffect(() => {
|
|
506
|
-
const sub = searchResults$.subscribe((result) => {
|
|
507
|
-
if (Result.isOk(result)) {
|
|
508
|
-
setResults(result.value);
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
return () => {
|
|
513
|
-
sub.unsubscribe();
|
|
514
|
-
};
|
|
515
|
-
}, []);
|
|
522
|
+
const searchResults = useObservableValue(searchResults$);
|
|
516
523
|
|
|
517
524
|
return (
|
|
518
525
|
<div>
|
|
@@ -523,7 +530,7 @@ const SearchBox = (): React.JSX.Element => {
|
|
|
523
530
|
}}
|
|
524
531
|
/>
|
|
525
532
|
<ul>
|
|
526
|
-
{
|
|
533
|
+
{searchResults.map((item) => (
|
|
527
534
|
<li key={item.id}>{item.name}</li>
|
|
528
535
|
))}
|
|
529
536
|
</ul>
|
|
@@ -532,315 +539,60 @@ const SearchBox = (): React.JSX.Element => {
|
|
|
532
539
|
};
|
|
533
540
|
```
|
|
534
541
|
|
|
535
|
-
### Advanced
|
|
536
|
-
|
|
537
|
-
For complex scenarios, SyncFlow provides observable-based APIs:
|
|
538
|
-
|
|
539
|
-
#### Creation Functions
|
|
540
|
-
|
|
541
|
-
- `source<T>()`: Create a new observable source
|
|
542
|
-
- `of(value)`: Create observable from a single value
|
|
543
|
-
- `fromArray(array)`: Create observable from array
|
|
544
|
-
- `fromPromise(promise)`: Create observable from promise
|
|
545
|
-
- `interval(ms)`: Emit values at intervals
|
|
546
|
-
- `timer(delay)`: Emit after delay
|
|
547
|
-
|
|
548
|
-
#### Operators
|
|
549
|
-
|
|
550
|
-
- `filter(predicate)`: Filter values
|
|
551
|
-
- `map(fn)`: Transform values
|
|
552
|
-
- `scan(reducer, seed)`: Accumulate values
|
|
553
|
-
- `debounceTime(ms)`: Debounce emissions
|
|
554
|
-
- `throttleTime(ms)`: Throttle emissions
|
|
555
|
-
- `skipIfNoChange()`: Skip duplicate values
|
|
556
|
-
- `takeUntil(notifier)`: Complete on notifier emission
|
|
557
|
-
|
|
558
|
-
#### Combination
|
|
559
|
-
|
|
560
|
-
- `combine(observables)`: Combine latest values from multiple sources
|
|
561
|
-
- `merge(observables)`: Merge multiple streams
|
|
562
|
-
- `zip(observables)`: Pair values by index
|
|
563
|
-
|
|
564
|
-
## Examples
|
|
565
|
-
|
|
566
|
-
### Global Counter State (React)
|
|
567
|
-
|
|
568
|
-
```tsx
|
|
569
|
-
import { createState } from 'synstate';
|
|
570
|
-
import { useState, useEffect } from 'react';
|
|
571
|
-
|
|
572
|
-
// Create global state
|
|
573
|
-
export const counterState = createState(0);
|
|
574
|
-
|
|
575
|
-
// Component 1
|
|
576
|
-
function Counter() {
|
|
577
|
-
const [count, setCount] = useState(counterState.getSnapshot());
|
|
578
|
-
|
|
579
|
-
useEffect(() => {
|
|
580
|
-
const sub = counterState.state.subscribe(setCount);
|
|
581
|
-
return () => sub.unsubscribe();
|
|
582
|
-
}, []);
|
|
583
|
-
|
|
584
|
-
return (
|
|
585
|
-
<div>
|
|
586
|
-
<p>Count: {count}</p>
|
|
587
|
-
<button onClick={() => counterState.updateState((n) => n + 1)}>
|
|
588
|
-
Increment
|
|
589
|
-
</button>
|
|
590
|
-
</div>
|
|
591
|
-
);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// Component 2 (synced automatically)
|
|
595
|
-
function ResetButton() {
|
|
596
|
-
return <button onClick={() => counterState.resetState()}>Reset</button>;
|
|
597
|
-
}
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
### Event-Driven Architecture (React)
|
|
601
|
-
|
|
602
|
-
```tsx
|
|
603
|
-
import { createValueEmitter } from 'synstate';
|
|
604
|
-
import { useEffect } from 'react';
|
|
605
|
-
|
|
606
|
-
// Global events
|
|
607
|
-
export const [userLoggedIn$, emitUserLoggedIn] = createValueEmitter<{
|
|
608
|
-
id: number;
|
|
609
|
-
name: string;
|
|
610
|
-
}>();
|
|
611
|
-
|
|
612
|
-
export const [userLoggedOut$, emitUserLoggedOut] = createEventEmitter();
|
|
613
|
-
|
|
614
|
-
// Component that emits events
|
|
615
|
-
function LoginButton() {
|
|
616
|
-
const handleLogin = async () => {
|
|
617
|
-
const user = await loginUser();
|
|
618
|
-
emitUserLoggedIn(user);
|
|
619
|
-
};
|
|
620
|
-
|
|
621
|
-
return <button onClick={handleLogin}>Login</button>;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Component that listens to events
|
|
625
|
-
function Notification() {
|
|
626
|
-
const [message, setMessage] = useState('');
|
|
627
|
-
|
|
628
|
-
useEffect(() => {
|
|
629
|
-
const sub1 = userLoggedIn$.subscribe((user) => {
|
|
630
|
-
setMessage(`Welcome, ${user.name}!`);
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
const sub2 = userLoggedOut$.subscribe(() => {
|
|
634
|
-
setMessage('Logged out');
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
return () => {
|
|
638
|
-
sub1.unsubscribe();
|
|
639
|
-
sub2.unsubscribe();
|
|
640
|
-
};
|
|
641
|
-
}, []);
|
|
642
|
-
|
|
643
|
-
return message ? <div className="notification">{message}</div> : null;
|
|
644
|
-
}
|
|
645
|
-
```
|
|
646
|
-
|
|
647
|
-
### Todo List with Reducer (React)
|
|
542
|
+
### Advanced: Event Emitter with Throttle
|
|
648
543
|
|
|
649
544
|
```tsx
|
|
650
|
-
import {
|
|
651
|
-
import { useState, useEffect } from 'react';
|
|
652
|
-
|
|
653
|
-
type Todo = { id: number; text: string; done: boolean };
|
|
654
|
-
type Action =
|
|
655
|
-
| { type: 'add'; text: string }
|
|
656
|
-
| { type: 'toggle'; id: number }
|
|
657
|
-
| { type: 'remove'; id: number };
|
|
658
|
-
|
|
659
|
-
const todoState = createReducer<Todo[], Action>((todos, action) => {
|
|
660
|
-
switch (action.type) {
|
|
661
|
-
case 'add':
|
|
662
|
-
return [
|
|
663
|
-
...todos,
|
|
664
|
-
{
|
|
665
|
-
id: Date.now(),
|
|
666
|
-
text: action.text,
|
|
667
|
-
done: false,
|
|
668
|
-
},
|
|
669
|
-
];
|
|
670
|
-
case 'toggle':
|
|
671
|
-
return todos.map((t) =>
|
|
672
|
-
t.id === action.id ? { ...t, done: !t.done } : t,
|
|
673
|
-
);
|
|
674
|
-
case 'remove':
|
|
675
|
-
return todos.filter((t) => t.id !== action.id);
|
|
676
|
-
}
|
|
677
|
-
}, []);
|
|
678
|
-
|
|
679
|
-
function TodoList() {
|
|
680
|
-
const [todos, setTodos] = useState(todoState.getSnapshot());
|
|
681
|
-
|
|
682
|
-
useEffect(() => {
|
|
683
|
-
const sub = todoState.state.subscribe(setTodos);
|
|
684
|
-
return () => sub.unsubscribe();
|
|
685
|
-
}, []);
|
|
545
|
+
import { createEventEmitter, throttleTime } from 'synstate';
|
|
686
546
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
{todos.map((todo) => (
|
|
690
|
-
<div key={todo.id}>
|
|
691
|
-
<input
|
|
692
|
-
type="checkbox"
|
|
693
|
-
checked={todo.done}
|
|
694
|
-
onChange={() =>
|
|
695
|
-
todoState.dispatch({
|
|
696
|
-
type: 'toggle',
|
|
697
|
-
id: todo.id,
|
|
698
|
-
})
|
|
699
|
-
}
|
|
700
|
-
/>
|
|
701
|
-
<span>{todo.text}</span>
|
|
702
|
-
</div>
|
|
703
|
-
))}
|
|
704
|
-
<button
|
|
705
|
-
onClick={() =>
|
|
706
|
-
todoState.dispatch({
|
|
707
|
-
type: 'add',
|
|
708
|
-
text: 'New Todo',
|
|
709
|
-
})
|
|
710
|
-
}
|
|
711
|
-
>
|
|
712
|
-
Add Todo
|
|
713
|
-
</button>
|
|
714
|
-
</div>
|
|
715
|
-
);
|
|
716
|
-
}
|
|
717
|
-
```
|
|
718
|
-
|
|
719
|
-
### Boolean State (Dark Mode)
|
|
720
|
-
|
|
721
|
-
```tsx
|
|
722
|
-
import { createBooleanState } from 'synstate';
|
|
723
|
-
import { useState, useEffect } from 'react';
|
|
724
|
-
|
|
725
|
-
export const darkModeState = createBooleanState(false);
|
|
726
|
-
|
|
727
|
-
function ThemeToggle() {
|
|
728
|
-
const [isDark, setIsDark] = useState(darkModeState.getSnapshot());
|
|
729
|
-
|
|
730
|
-
useEffect(() => {
|
|
731
|
-
const sub = darkModeState.state.subscribe(setIsDark);
|
|
732
|
-
return () => sub.unsubscribe();
|
|
733
|
-
}, []);
|
|
734
|
-
|
|
735
|
-
useEffect(() => {
|
|
736
|
-
document.body.className = isDark ? 'dark' : 'light';
|
|
737
|
-
}, [isDark]);
|
|
738
|
-
|
|
739
|
-
return (
|
|
740
|
-
<button onClick={() => darkModeState.toggle()}>
|
|
741
|
-
{isDark ? '🌙' : '☀️'}
|
|
742
|
-
</button>
|
|
743
|
-
);
|
|
744
|
-
}
|
|
745
|
-
```
|
|
746
|
-
|
|
747
|
-
### Cross-Component Communication
|
|
748
|
-
|
|
749
|
-
```tsx
|
|
750
|
-
import { createValueEmitter, createState } from 'synstate';
|
|
751
|
-
import { useState, useEffect } from 'react';
|
|
752
|
-
```
|
|
753
|
-
|
|
754
|
-
// Events
|
|
755
|
-
|
|
756
|
-
### Advanced: Search with Debounce
|
|
757
|
-
|
|
758
|
-
```tsx
|
|
759
|
-
import * as React from 'react';
|
|
760
|
-
import {
|
|
761
|
-
createState,
|
|
762
|
-
debounceTime,
|
|
763
|
-
filter,
|
|
764
|
-
fromPromise,
|
|
765
|
-
type Observable,
|
|
766
|
-
switchMap,
|
|
767
|
-
} from 'synstate';
|
|
768
|
-
import { Result } from 'ts-data-forge';
|
|
769
|
-
|
|
770
|
-
const [searchState, setSearchState] = createState('');
|
|
771
|
-
|
|
772
|
-
// Advanced reactive pipeline (optional feature)
|
|
773
|
-
const searchResults$: Observable<
|
|
774
|
-
Result<readonly Readonly<{ id: string; name: string }>[], unknown>
|
|
775
|
-
> = searchState
|
|
776
|
-
.pipe(debounceTime(300))
|
|
777
|
-
.pipe(filter((query) => query.length > 2))
|
|
778
|
-
.pipe(
|
|
779
|
-
switchMap((query) =>
|
|
780
|
-
fromPromise(
|
|
781
|
-
fetch(`/api/search?q=${query}`).then(
|
|
782
|
-
(r) =>
|
|
783
|
-
r.json() as Promise<
|
|
784
|
-
readonly Readonly<{ id: string; name: string }>[]
|
|
785
|
-
>,
|
|
786
|
-
),
|
|
787
|
-
),
|
|
788
|
-
),
|
|
789
|
-
);
|
|
547
|
+
// Create event emitter
|
|
548
|
+
const [refreshClicked, onRefreshClick] = createEventEmitter();
|
|
790
549
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
550
|
+
// Subscribe to events
|
|
551
|
+
refreshClicked.subscribe(() => {
|
|
552
|
+
console.log('Refresh Clicked');
|
|
553
|
+
});
|
|
795
554
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
if (Result.isOk(result)) {
|
|
799
|
-
setResults(result.value);
|
|
800
|
-
}
|
|
801
|
-
});
|
|
555
|
+
// Throttle refresh clicks to prevent rapid successive executions
|
|
556
|
+
const throttledRefresh = refreshClicked.pipe(throttleTime(2000));
|
|
802
557
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
558
|
+
throttledRefresh.subscribe(() => {
|
|
559
|
+
console.log('Executing refresh...');
|
|
560
|
+
// Actual refresh logic here
|
|
561
|
+
// This will be called at most once every 2 seconds
|
|
562
|
+
});
|
|
807
563
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
{results.map((item) => (
|
|
818
|
-
<li key={item.id}>{item.name}</li>
|
|
819
|
-
))}
|
|
820
|
-
</ul>
|
|
821
|
-
</div>
|
|
822
|
-
);
|
|
823
|
-
};
|
|
564
|
+
const DataTable = (): React.JSX.Element => (
|
|
565
|
+
<div>
|
|
566
|
+
<button onClick={onRefreshClick}>{'Refresh'}</button>
|
|
567
|
+
<p>
|
|
568
|
+
{'Data: '}
|
|
569
|
+
{/* Display data here */}
|
|
570
|
+
</p>
|
|
571
|
+
</div>
|
|
572
|
+
);
|
|
824
573
|
```
|
|
825
574
|
|
|
826
|
-
## Why
|
|
575
|
+
## Why SynState?
|
|
827
576
|
|
|
828
577
|
### Simple State Management, Not Complex Reactive Programming
|
|
829
578
|
|
|
830
|
-
|
|
579
|
+
SynState is a state management library for web frontends, similar to Redux, Jotai, Zustand, and MobX. It provides APIs for creating and managing global state across your application.
|
|
580
|
+
|
|
581
|
+
Under the hood, SynState is built on Observable patterns similar to those provided by RxJS. However, unlike RxJS, which can make code harder to read with many operators and complex streams, SynState focuses on **simple, readable state management and event handling**. Most applications only need `createState`, `createReducer`, and simple operators/combinators like `combine` and `map` — clean, straightforward APIs that developers understand immediately.
|
|
831
582
|
|
|
832
583
|
**Advanced reactive features are optional** and only used when you actually need them (like debouncing search input). The library doesn't force you into a reactive programming mindset.
|
|
833
584
|
|
|
834
585
|
### Key Differences from RxJS
|
|
835
586
|
|
|
836
|
-
- **Focus on State
|
|
837
|
-
- **
|
|
587
|
+
- **Focus on State Management**: Designed specifically for state management, not just asynchronous event processing
|
|
588
|
+
- **InitializedObservable**: Provides `InitializedObservable` which always holds an initial value, making it ideal for representing state
|
|
589
|
+
- **Simpler API**: Most use cases are covered by `createState`, `createReducer`, and `createEventEmitter`
|
|
838
590
|
- **Better Readability**: No need for complex operator chains in everyday code
|
|
839
|
-
- **Optional Complexity**: Advanced features available when needed
|
|
591
|
+
- **Optional Complexity**: Advanced features available to manipulate Observables when needed
|
|
840
592
|
|
|
841
593
|
### Use Cases
|
|
842
594
|
|
|
843
|
-
**Use
|
|
595
|
+
**Use SynState when you need:**
|
|
844
596
|
|
|
845
597
|
- ✅ Global state management across components
|
|
846
598
|
- ✅ Event-driven communication between components
|
|
@@ -850,24 +602,12 @@ Unlike RxJS, which can make code harder to read with many operators and complex
|
|
|
850
602
|
|
|
851
603
|
**Consider other solutions when:**
|
|
852
604
|
|
|
853
|
-
- ❌ You need
|
|
605
|
+
- ❌ You need state in a React component (use React hooks `useState`, `useReducer`)
|
|
854
606
|
- ❌ Your app is simple enough for React Context alone
|
|
855
607
|
|
|
856
608
|
## Type Safety
|
|
857
609
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
```tsx
|
|
861
|
-
const userState = createState({ name: 'Alice', age: 25 });
|
|
862
|
-
// state type: Observable<{ name: string; age: number }>
|
|
863
|
-
|
|
864
|
-
const snapshot = userState.getSnapshot();
|
|
865
|
-
// snapshot type: { name: string; age: number }
|
|
866
|
-
|
|
867
|
-
const [onClick$, emitClick] = createValueEmitter<MouseEvent>();
|
|
868
|
-
// onClick$ type: Observable<MouseEvent>
|
|
869
|
-
// emitClick type: (event: MouseEvent) => void
|
|
870
|
-
```
|
|
610
|
+
SynState maintains full type information.
|
|
871
611
|
|
|
872
612
|
## License
|
|
873
613
|
|