synstate 0.1.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 +878 -0
- package/dist/core/class/child-observable-class.d.mts +37 -0
- package/dist/core/class/child-observable-class.d.mts.map +1 -0
- package/dist/core/class/child-observable-class.mjs +134 -0
- package/dist/core/class/child-observable-class.mjs.map +1 -0
- package/dist/core/class/index.d.mts +4 -0
- package/dist/core/class/index.d.mts.map +1 -0
- package/dist/core/class/index.mjs +4 -0
- package/dist/core/class/index.mjs.map +1 -0
- package/dist/core/class/observable-base-class.d.mts +28 -0
- package/dist/core/class/observable-base-class.d.mts.map +1 -0
- package/dist/core/class/observable-base-class.mjs +116 -0
- package/dist/core/class/observable-base-class.mjs.map +1 -0
- package/dist/core/class/root-observable-class.d.mts +12 -0
- package/dist/core/class/root-observable-class.d.mts.map +1 -0
- package/dist/core/class/root-observable-class.mjs +35 -0
- package/dist/core/class/root-observable-class.mjs.map +1 -0
- package/dist/core/combine/combine.d.mts +35 -0
- package/dist/core/combine/combine.d.mts.map +1 -0
- package/dist/core/combine/combine.mjs +94 -0
- package/dist/core/combine/combine.mjs.map +1 -0
- package/dist/core/combine/index.d.mts +4 -0
- package/dist/core/combine/index.d.mts.map +1 -0
- package/dist/core/combine/index.mjs +4 -0
- package/dist/core/combine/index.mjs.map +1 -0
- package/dist/core/combine/merge.d.mts +28 -0
- package/dist/core/combine/merge.d.mts.map +1 -0
- package/dist/core/combine/merge.mjs +52 -0
- package/dist/core/combine/merge.mjs.map +1 -0
- package/dist/core/combine/zip.d.mts +26 -0
- package/dist/core/combine/zip.d.mts.map +1 -0
- package/dist/core/combine/zip.mjs +63 -0
- package/dist/core/combine/zip.mjs.map +1 -0
- package/dist/core/create/from-array.d.mts +21 -0
- package/dist/core/create/from-array.d.mts.map +1 -0
- package/dist/core/create/from-array.mjs +47 -0
- package/dist/core/create/from-array.mjs.map +1 -0
- package/dist/core/create/from-promise.d.mts +25 -0
- package/dist/core/create/from-promise.d.mts.map +1 -0
- package/dist/core/create/from-promise.mjs +51 -0
- package/dist/core/create/from-promise.mjs.map +1 -0
- package/dist/core/create/from-subscribable.d.mts +3 -0
- package/dist/core/create/from-subscribable.d.mts.map +1 -0
- package/dist/core/create/from-subscribable.mjs +22 -0
- package/dist/core/create/from-subscribable.mjs.map +1 -0
- package/dist/core/create/index.d.mts +8 -0
- package/dist/core/create/index.d.mts.map +1 -0
- package/dist/core/create/index.mjs +8 -0
- package/dist/core/create/index.mjs.map +1 -0
- package/dist/core/create/interval.d.mts +21 -0
- package/dist/core/create/interval.d.mts.map +1 -0
- package/dist/core/create/interval.mjs +74 -0
- package/dist/core/create/interval.mjs.map +1 -0
- package/dist/core/create/of.d.mts +20 -0
- package/dist/core/create/of.d.mts.map +1 -0
- package/dist/core/create/of.mjs +44 -0
- package/dist/core/create/of.mjs.map +1 -0
- package/dist/core/create/source.d.mts +29 -0
- package/dist/core/create/source.d.mts.map +1 -0
- package/dist/core/create/source.mjs +29 -0
- package/dist/core/create/source.mjs.map +1 -0
- package/dist/core/create/timer.d.mts +20 -0
- package/dist/core/create/timer.d.mts.map +1 -0
- package/dist/core/create/timer.mjs +64 -0
- package/dist/core/create/timer.mjs.map +1 -0
- package/dist/core/index.d.mts +7 -0
- package/dist/core/index.d.mts.map +1 -0
- package/dist/core/index.mjs +37 -0
- package/dist/core/index.mjs.map +1 -0
- package/dist/core/operators/audit-time.d.mts +3 -0
- package/dist/core/operators/audit-time.d.mts.map +1 -0
- package/dist/core/operators/audit-time.mjs +50 -0
- package/dist/core/operators/audit-time.mjs.map +1 -0
- package/dist/core/operators/debounce-time.d.mts +31 -0
- package/dist/core/operators/debounce-time.d.mts.map +1 -0
- package/dist/core/operators/debounce-time.mjs +73 -0
- package/dist/core/operators/debounce-time.mjs.map +1 -0
- package/dist/core/operators/filter.d.mts +28 -0
- package/dist/core/operators/filter.d.mts.map +1 -0
- package/dist/core/operators/filter.mjs +38 -0
- package/dist/core/operators/filter.mjs.map +1 -0
- package/dist/core/operators/index.d.mts +18 -0
- package/dist/core/operators/index.d.mts.map +1 -0
- package/dist/core/operators/index.mjs +18 -0
- package/dist/core/operators/index.mjs.map +1 -0
- package/dist/core/operators/map-with-index.d.mts +39 -0
- package/dist/core/operators/map-with-index.d.mts.map +1 -0
- package/dist/core/operators/map-with-index.mjs +73 -0
- package/dist/core/operators/map-with-index.mjs.map +1 -0
- package/dist/core/operators/merge-map.d.mts +34 -0
- package/dist/core/operators/merge-map.d.mts.map +1 -0
- package/dist/core/operators/merge-map.mjs +75 -0
- package/dist/core/operators/merge-map.mjs.map +1 -0
- package/dist/core/operators/pairwise.d.mts +27 -0
- package/dist/core/operators/pairwise.d.mts.map +1 -0
- package/dist/core/operators/pairwise.mjs +59 -0
- package/dist/core/operators/pairwise.mjs.map +1 -0
- package/dist/core/operators/scan.d.mts +30 -0
- package/dist/core/operators/scan.d.mts.map +1 -0
- package/dist/core/operators/scan.mjs +56 -0
- package/dist/core/operators/scan.mjs.map +1 -0
- package/dist/core/operators/skip-if-no-change.d.mts +33 -0
- package/dist/core/operators/skip-if-no-change.d.mts.map +1 -0
- package/dist/core/operators/skip-if-no-change.mjs +68 -0
- package/dist/core/operators/skip-if-no-change.mjs.map +1 -0
- package/dist/core/operators/skip-until.d.mts +3 -0
- package/dist/core/operators/skip-until.d.mts.map +1 -0
- package/dist/core/operators/skip-until.mjs +33 -0
- package/dist/core/operators/skip-until.mjs.map +1 -0
- package/dist/core/operators/skip-while.d.mts +4 -0
- package/dist/core/operators/skip-while.d.mts.map +1 -0
- package/dist/core/operators/skip-while.mjs +40 -0
- package/dist/core/operators/skip-while.mjs.map +1 -0
- package/dist/core/operators/switch-map.d.mts +31 -0
- package/dist/core/operators/switch-map.d.mts.map +1 -0
- package/dist/core/operators/switch-map.mjs +70 -0
- package/dist/core/operators/switch-map.mjs.map +1 -0
- package/dist/core/operators/take-until.d.mts +32 -0
- package/dist/core/operators/take-until.d.mts.map +1 -0
- package/dist/core/operators/take-until.mjs +60 -0
- package/dist/core/operators/take-until.mjs.map +1 -0
- package/dist/core/operators/take-while.d.mts +4 -0
- package/dist/core/operators/take-while.d.mts.map +1 -0
- package/dist/core/operators/take-while.mjs +42 -0
- package/dist/core/operators/take-while.mjs.map +1 -0
- package/dist/core/operators/throttle-time.d.mts +23 -0
- package/dist/core/operators/throttle-time.d.mts.map +1 -0
- package/dist/core/operators/throttle-time.mjs +68 -0
- package/dist/core/operators/throttle-time.mjs.map +1 -0
- package/dist/core/operators/with-buffered-from.d.mts +4 -0
- package/dist/core/operators/with-buffered-from.d.mts.map +1 -0
- package/dist/core/operators/with-buffered-from.mjs +45 -0
- package/dist/core/operators/with-buffered-from.mjs.map +1 -0
- package/dist/core/operators/with-current-value-from.d.mts +4 -0
- package/dist/core/operators/with-current-value-from.d.mts.map +1 -0
- package/dist/core/operators/with-current-value-from.mjs +37 -0
- package/dist/core/operators/with-current-value-from.mjs.map +1 -0
- package/dist/core/operators/with-initial-value.d.mts +26 -0
- package/dist/core/operators/with-initial-value.d.mts.map +1 -0
- package/dist/core/operators/with-initial-value.mjs +47 -0
- package/dist/core/operators/with-initial-value.mjs.map +1 -0
- package/dist/core/types/id.d.mts +4 -0
- package/dist/core/types/id.d.mts.map +1 -0
- package/dist/core/types/id.mjs +2 -0
- package/dist/core/types/id.mjs.map +1 -0
- package/dist/core/types/index.d.mts +6 -0
- package/dist/core/types/index.d.mts.map +1 -0
- package/dist/core/types/index.mjs +3 -0
- package/dist/core/types/index.mjs.map +1 -0
- package/dist/core/types/observable-family.d.mts +68 -0
- package/dist/core/types/observable-family.d.mts.map +1 -0
- package/dist/core/types/observable-family.mjs +2 -0
- package/dist/core/types/observable-family.mjs.map +1 -0
- package/dist/core/types/observable-kind.d.mts +4 -0
- package/dist/core/types/observable-kind.d.mts.map +1 -0
- package/dist/core/types/observable-kind.mjs +2 -0
- package/dist/core/types/observable-kind.mjs.map +1 -0
- package/dist/core/types/observable.d.mts +83 -0
- package/dist/core/types/observable.d.mts.map +1 -0
- package/dist/core/types/observable.mjs +10 -0
- package/dist/core/types/observable.mjs.map +1 -0
- package/dist/core/types/types.d.mts +16 -0
- package/dist/core/types/types.d.mts.map +1 -0
- package/dist/core/types/types.mjs +2 -0
- package/dist/core/types/types.mjs.map +1 -0
- package/dist/core/utils/id-maker.d.mts +5 -0
- package/dist/core/utils/id-maker.d.mts.map +1 -0
- package/dist/core/utils/id-maker.mjs +17 -0
- package/dist/core/utils/id-maker.mjs.map +1 -0
- package/dist/core/utils/index.d.mts +5 -0
- package/dist/core/utils/index.d.mts.map +1 -0
- package/dist/core/utils/index.mjs +5 -0
- package/dist/core/utils/index.mjs.map +1 -0
- package/dist/core/utils/max-depth.d.mts +3 -0
- package/dist/core/utils/max-depth.d.mts.map +1 -0
- package/dist/core/utils/max-depth.mjs +8 -0
- package/dist/core/utils/max-depth.mjs.map +1 -0
- package/dist/core/utils/observable-utils.d.mts +3 -0
- package/dist/core/utils/observable-utils.d.mts.map +1 -0
- package/dist/core/utils/observable-utils.mjs +7 -0
- package/dist/core/utils/observable-utils.mjs.map +1 -0
- package/dist/core/utils/utils.d.mts +4 -0
- package/dist/core/utils/utils.d.mts.map +1 -0
- package/dist/core/utils/utils.mjs +38 -0
- package/dist/core/utils/utils.mjs.map +1 -0
- package/dist/entry-point.d.mts +2 -0
- package/dist/entry-point.d.mts.map +1 -0
- package/dist/entry-point.mjs +40 -0
- package/dist/entry-point.mjs.map +1 -0
- package/dist/globals.d.mts +4 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +40 -0
- package/dist/index.mjs.map +1 -0
- package/dist/tsconfig.json +1 -0
- package/dist/types.d.mts +2 -0
- package/dist/utils/create-event-emitter.d.mts +39 -0
- package/dist/utils/create-event-emitter.d.mts.map +1 -0
- package/dist/utils/create-event-emitter.mjs +57 -0
- package/dist/utils/create-event-emitter.mjs.map +1 -0
- package/dist/utils/create-reducer.d.mts +34 -0
- package/dist/utils/create-reducer.d.mts.map +1 -0
- package/dist/utils/create-reducer.mjs +49 -0
- package/dist/utils/create-reducer.mjs.map +1 -0
- package/dist/utils/create-state.d.mts +61 -0
- package/dist/utils/create-state.d.mts.map +1 -0
- package/dist/utils/create-state.mjs +92 -0
- package/dist/utils/create-state.mjs.map +1 -0
- package/dist/utils/index.d.mts +4 -0
- package/dist/utils/index.d.mts.map +1 -0
- package/dist/utils/index.mjs +4 -0
- package/dist/utils/index.mjs.map +1 -0
- package/package.json +71 -0
- package/src/core/class/child-observable-class.mts +232 -0
- package/src/core/class/index.mts +3 -0
- package/src/core/class/observable-base-class.mts +186 -0
- package/src/core/class/observable.class.test.mts +89 -0
- package/src/core/class/root-observable-class.mts +68 -0
- package/src/core/combine/combine.mts +144 -0
- package/src/core/combine/index.mts +3 -0
- package/src/core/combine/merge.mts +84 -0
- package/src/core/combine/zip.mts +149 -0
- package/src/core/create/from-array.mts +58 -0
- package/src/core/create/from-promise.mts +58 -0
- package/src/core/create/from-subscribable.mts +37 -0
- package/src/core/create/index.mts +7 -0
- package/src/core/create/interval.mts +99 -0
- package/src/core/create/of.mts +54 -0
- package/src/core/create/source.mts +59 -0
- package/src/core/create/timer.mts +84 -0
- package/src/core/index.mts +6 -0
- package/src/core/operators/audit-time.mts +77 -0
- package/src/core/operators/debounce-time.mts +96 -0
- package/src/core/operators/filter.mts +125 -0
- package/src/core/operators/index.mts +17 -0
- package/src/core/operators/map-with-index.mts +168 -0
- package/src/core/operators/merge-map.mts +108 -0
- package/src/core/operators/pairwise.mts +77 -0
- package/src/core/operators/scan.mts +81 -0
- package/src/core/operators/skip-if-no-change.mts +91 -0
- package/src/core/operators/skip-until.mts +54 -0
- package/src/core/operators/skip-while.mts +77 -0
- package/src/core/operators/switch-map.mts +101 -0
- package/src/core/operators/take-until.mts +80 -0
- package/src/core/operators/take-while.mts +103 -0
- package/src/core/operators/throttle-time.mts +95 -0
- package/src/core/operators/with-buffered-from.mts +68 -0
- package/src/core/operators/with-current-value-from.mts +58 -0
- package/src/core/operators/with-initial-value.mts +76 -0
- package/src/core/types/id.mts +5 -0
- package/src/core/types/index.mts +5 -0
- package/src/core/types/observable-family.mts +259 -0
- package/src/core/types/observable-kind.mts +5 -0
- package/src/core/types/observable.mts +218 -0
- package/src/core/types/types.mts +40 -0
- package/src/core/utils/id-maker.mts +31 -0
- package/src/core/utils/index.mts +4 -0
- package/src/core/utils/max-depth.mts +7 -0
- package/src/core/utils/observable-utils.mts +10 -0
- package/src/core/utils/utils.mts +51 -0
- package/src/core/utils/utils.test.mts +88 -0
- package/src/entry-point.mts +1 -0
- package/src/globals.d.mts +4 -0
- package/src/index.mts +2 -0
- package/src/utils/create-event-emitter.mts +62 -0
- package/src/utils/create-reducer.mts +55 -0
- package/src/utils/create-state.mts +138 -0
- package/src/utils/index.mts +3 -0
package/README.md
ADDED
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
# SyncFlow
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/ts-data-forge)
|
|
4
|
+
[](https://www.npmjs.com/package/ts-data-forge)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
[](https://codecov.io/gh/noshiro-pf/ts-data-forge)
|
|
7
|
+
|
|
8
|
+
**SyncFlow** is a lightweight, type-safe state management library for TypeScript/JavaScript. Perfect for building reactive global state and event-driven systems in React, Vue, and other frameworks.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- 🎯 **Simple State Management**: Easy-to-use `createState` and `createReducer` for global state
|
|
13
|
+
- 📡 **Event System**: Built-in `createValueEmitter`, `createEventEmitter` for event-driven architecture
|
|
14
|
+
- 🔄 **Reactive Updates**: Automatic propagation of state changes to subscribers
|
|
15
|
+
- 🎨 **Type-Safe**: Full TypeScript support with precise type inference
|
|
16
|
+
- 🚀 **Lightweight**: Minimal bundle size, zero external runtime dependencies
|
|
17
|
+
- ⚡ **Framework Agnostic**: Works with React, Vue, Svelte, or vanilla JavaScript
|
|
18
|
+
- 🔧 **Flexible**: Simple state management with optional advanced features
|
|
19
|
+
|
|
20
|
+
## Documentation
|
|
21
|
+
|
|
22
|
+
- API reference: <https://noshiro-pf.github.io/synstate/>
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm add synstate
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or with other package managers:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Yarn
|
|
34
|
+
yarn add synstate
|
|
35
|
+
|
|
36
|
+
# pnpm
|
|
37
|
+
pnpm add synstate
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
### Simple State Management
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { createState } from 'synstate';
|
|
46
|
+
|
|
47
|
+
// Create a reactive state
|
|
48
|
+
const [state, setState, { updateState }] = createState(0);
|
|
49
|
+
|
|
50
|
+
// Subscribe to changes (in React components, Vue watchers, etc.)
|
|
51
|
+
state.subscribe((count: number) => {
|
|
52
|
+
console.log('Count:', count);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Update state
|
|
56
|
+
setState(1);
|
|
57
|
+
|
|
58
|
+
updateState((prev: number) => prev + 1);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Event Emitter
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { createValueEmitter } from 'synstate';
|
|
65
|
+
|
|
66
|
+
type User = Readonly<{ id: number; name: string }>;
|
|
67
|
+
|
|
68
|
+
// Create event emitter
|
|
69
|
+
const [userLoggedIn$, emitUserLoggedIn] = createValueEmitter<User>();
|
|
70
|
+
|
|
71
|
+
// Subscribe to events
|
|
72
|
+
userLoggedIn$.subscribe((user) => {
|
|
73
|
+
console.log('User logged in:', user.name);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Emit events
|
|
77
|
+
emitUserLoggedIn({ id: 1, name: 'Alice' });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### With React
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import * as React from 'react';
|
|
84
|
+
import { createState } from 'synstate';
|
|
85
|
+
|
|
86
|
+
// Global state (outside component)
|
|
87
|
+
const [userState, setUserState, { getSnapshot }] = createState({
|
|
88
|
+
name: '',
|
|
89
|
+
email: '',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const UserProfile = (): React.JSX.Element => {
|
|
93
|
+
const [user, setUser] = React.useState(getSnapshot());
|
|
94
|
+
|
|
95
|
+
React.useEffect(() => {
|
|
96
|
+
const subscription = userState.subscribe(setUser);
|
|
97
|
+
|
|
98
|
+
return () => {
|
|
99
|
+
subscription.unsubscribe();
|
|
100
|
+
};
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div>
|
|
105
|
+
<p>
|
|
106
|
+
{'Name: '}
|
|
107
|
+
{user.name}
|
|
108
|
+
</p>
|
|
109
|
+
<button
|
|
110
|
+
onClick={() => {
|
|
111
|
+
setUserState({
|
|
112
|
+
name: 'Alice',
|
|
113
|
+
email: 'alice@example.com',
|
|
114
|
+
});
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
{'Set User'}
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Core Concepts
|
|
125
|
+
|
|
126
|
+
### State Management
|
|
127
|
+
|
|
128
|
+
SyncFlow provides simple, intuitive APIs for managing application state:
|
|
129
|
+
|
|
130
|
+
- **`createState`**: Create mutable state with getter/setter
|
|
131
|
+
- **`createReducer`**: Redux-style state management
|
|
132
|
+
- **`createBooleanState`**: Specialized state for boolean values
|
|
133
|
+
|
|
134
|
+
### Event System
|
|
135
|
+
|
|
136
|
+
Built-in event emitter for event-driven patterns:
|
|
137
|
+
|
|
138
|
+
- **`createValueEmitter`**: Create type-safe event emitters
|
|
139
|
+
- **`createEventEmitter`**: Create event emitters without payload
|
|
140
|
+
|
|
141
|
+
### Observable (Optional Advanced Feature)
|
|
142
|
+
|
|
143
|
+
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
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
import * as React from 'react';
|
|
147
|
+
import { createState } from 'synstate';
|
|
148
|
+
|
|
149
|
+
// Create global state
|
|
150
|
+
export const [counterState, , { updateState, resetState, getSnapshot }] =
|
|
151
|
+
createState(0);
|
|
152
|
+
|
|
153
|
+
// Component 1
|
|
154
|
+
const Counter = (): React.JSX.Element => {
|
|
155
|
+
const [count, setCount] = React.useState(getSnapshot());
|
|
156
|
+
|
|
157
|
+
React.useEffect(() => {
|
|
158
|
+
const sub = counterState.subscribe(setCount);
|
|
159
|
+
|
|
160
|
+
return () => {
|
|
161
|
+
sub.unsubscribe();
|
|
162
|
+
};
|
|
163
|
+
}, []);
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div>
|
|
167
|
+
<p>
|
|
168
|
+
{'Count: '}
|
|
169
|
+
{count}
|
|
170
|
+
</p>
|
|
171
|
+
<button
|
|
172
|
+
onClick={() => {
|
|
173
|
+
updateState((n: number) => n + 1);
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
{'Increment'}
|
|
177
|
+
</button>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Component 2 (synced automatically)
|
|
183
|
+
const ResetButton = (): React.JSX.Element => (
|
|
184
|
+
<button
|
|
185
|
+
onClick={() => {
|
|
186
|
+
resetState();
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
{'Reset'}
|
|
190
|
+
</button>
|
|
191
|
+
);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## API Reference
|
|
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:
|
|
267
|
+
|
|
268
|
+
```tsx
|
|
269
|
+
import * as React from 'react';
|
|
270
|
+
import { createReducer } from 'synstate';
|
|
271
|
+
|
|
272
|
+
type Todo = Readonly<{ id: number; text: string; done: boolean }>;
|
|
273
|
+
|
|
274
|
+
type Action = Readonly<
|
|
275
|
+
| { type: 'add'; text: string }
|
|
276
|
+
| { type: 'toggle'; id: number }
|
|
277
|
+
| { type: 'remove'; id: number }
|
|
278
|
+
>;
|
|
279
|
+
|
|
280
|
+
const [todoState, dispatch, getSnapshot] = createReducer<
|
|
281
|
+
readonly Todo[],
|
|
282
|
+
Action
|
|
283
|
+
>((todos, action) => {
|
|
284
|
+
switch (action.type) {
|
|
285
|
+
case 'add':
|
|
286
|
+
return [
|
|
287
|
+
...todos,
|
|
288
|
+
{
|
|
289
|
+
id: Date.now(),
|
|
290
|
+
text: action.text,
|
|
291
|
+
done: false,
|
|
292
|
+
},
|
|
293
|
+
];
|
|
294
|
+
case 'toggle':
|
|
295
|
+
return todos.map((t) =>
|
|
296
|
+
t.id === action.id ? { ...t, done: !t.done } : t,
|
|
297
|
+
);
|
|
298
|
+
case 'remove':
|
|
299
|
+
return todos.filter((t) => t.id !== action.id);
|
|
300
|
+
}
|
|
301
|
+
}, []);
|
|
302
|
+
|
|
303
|
+
const TodoList = (): React.JSX.Element => {
|
|
304
|
+
const [todos, setTodos] = React.useState(getSnapshot());
|
|
305
|
+
|
|
306
|
+
React.useEffect(() => {
|
|
307
|
+
const sub = todoState.subscribe(setTodos);
|
|
308
|
+
|
|
309
|
+
return () => {
|
|
310
|
+
sub.unsubscribe();
|
|
311
|
+
};
|
|
312
|
+
}, []);
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<div>
|
|
316
|
+
{todos.map((todo) => (
|
|
317
|
+
<div key={todo.id}>
|
|
318
|
+
<input
|
|
319
|
+
checked={todo.done}
|
|
320
|
+
type={'checkbox'}
|
|
321
|
+
onChange={() => {
|
|
322
|
+
dispatch({
|
|
323
|
+
type: 'toggle',
|
|
324
|
+
id: todo.id,
|
|
325
|
+
});
|
|
326
|
+
}}
|
|
327
|
+
/>
|
|
328
|
+
<span>{todo.text}</span>
|
|
329
|
+
</div>
|
|
330
|
+
))}
|
|
331
|
+
<button
|
|
332
|
+
onClick={() => {
|
|
333
|
+
dispatch({
|
|
334
|
+
type: 'add',
|
|
335
|
+
text: 'New Todo',
|
|
336
|
+
});
|
|
337
|
+
}}
|
|
338
|
+
>
|
|
339
|
+
{'Add Todo'}
|
|
340
|
+
</button>
|
|
341
|
+
</div>
|
|
342
|
+
);
|
|
343
|
+
};
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
#### createReducer
|
|
347
|
+
|
|
348
|
+
Create state with reducer pattern (like Redux):
|
|
349
|
+
|
|
350
|
+
```tsx
|
|
351
|
+
import * as React from 'react';
|
|
352
|
+
import { createBooleanState } from 'synstate';
|
|
353
|
+
|
|
354
|
+
export const [darkModeState, { toggle, getSnapshot }] =
|
|
355
|
+
createBooleanState(false);
|
|
356
|
+
|
|
357
|
+
const ThemeToggle = (): React.JSX.Element => {
|
|
358
|
+
const [isDark, setIsDark] = React.useState(getSnapshot());
|
|
359
|
+
|
|
360
|
+
React.useEffect(() => {
|
|
361
|
+
const sub = darkModeState.subscribe(setIsDark);
|
|
362
|
+
|
|
363
|
+
return () => {
|
|
364
|
+
sub.unsubscribe();
|
|
365
|
+
};
|
|
366
|
+
}, []);
|
|
367
|
+
|
|
368
|
+
React.useEffect(() => {
|
|
369
|
+
document.body.className = isDark ? 'dark' : 'light';
|
|
370
|
+
}, [isDark]);
|
|
371
|
+
|
|
372
|
+
return (
|
|
373
|
+
<button
|
|
374
|
+
onClick={() => {
|
|
375
|
+
toggle();
|
|
376
|
+
}}
|
|
377
|
+
>
|
|
378
|
+
{isDark ? '🌙' : '☀️'}
|
|
379
|
+
</button>
|
|
380
|
+
);
|
|
381
|
+
};
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Event System
|
|
385
|
+
|
|
386
|
+
#### createValueEmitter
|
|
387
|
+
|
|
388
|
+
Create type-safe event emitter with payload:
|
|
389
|
+
|
|
390
|
+
```tsx
|
|
391
|
+
import * as React from 'react';
|
|
392
|
+
import { createEventEmitter, createState, createValueEmitter } from 'synstate';
|
|
393
|
+
|
|
394
|
+
// Events
|
|
395
|
+
const [onItemAdded$, emitItemAdded] = createValueEmitter<string>();
|
|
396
|
+
|
|
397
|
+
const [onClearAll$, emitClearAll] = createEventEmitter();
|
|
398
|
+
|
|
399
|
+
// State
|
|
400
|
+
const [itemsState, setItemsState, { updateState, getSnapshot }] = createState<
|
|
401
|
+
readonly string[]
|
|
402
|
+
>([]);
|
|
403
|
+
|
|
404
|
+
// Setup event handlers
|
|
405
|
+
onItemAdded$.subscribe((item) => {
|
|
406
|
+
updateState((items: readonly string[]) => [...items, item]);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
onClearAll$.subscribe(() => {
|
|
410
|
+
setItemsState([]);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Component 1: Add items
|
|
414
|
+
const ItemInput = (): React.JSX.Element => {
|
|
415
|
+
const [input, setInput] = React.useState('');
|
|
416
|
+
|
|
417
|
+
return (
|
|
418
|
+
<div>
|
|
419
|
+
<input
|
|
420
|
+
value={input}
|
|
421
|
+
onChange={(e) => {
|
|
422
|
+
setInput(e.target.value);
|
|
423
|
+
}}
|
|
424
|
+
/>
|
|
425
|
+
<button
|
|
426
|
+
onClick={() => {
|
|
427
|
+
emitItemAdded(input);
|
|
428
|
+
|
|
429
|
+
setInput('');
|
|
430
|
+
}}
|
|
431
|
+
>
|
|
432
|
+
{'Add'}
|
|
433
|
+
</button>
|
|
434
|
+
</div>
|
|
435
|
+
);
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// Component 2: Display items
|
|
439
|
+
const ItemList = (): React.JSX.Element => {
|
|
440
|
+
const [items, setItems] = React.useState(getSnapshot());
|
|
441
|
+
|
|
442
|
+
React.useEffect(() => {
|
|
443
|
+
const sub = itemsState.subscribe(setItems);
|
|
444
|
+
|
|
445
|
+
return () => {
|
|
446
|
+
sub.unsubscribe();
|
|
447
|
+
};
|
|
448
|
+
}, []);
|
|
449
|
+
|
|
450
|
+
return (
|
|
451
|
+
<div>
|
|
452
|
+
<ul>
|
|
453
|
+
{items.map((item, i) => (
|
|
454
|
+
<li key={i}>{item}</li>
|
|
455
|
+
))}
|
|
456
|
+
</ul>
|
|
457
|
+
<button onClick={emitClearAll}>{'Clear All'}</button>
|
|
458
|
+
</div>
|
|
459
|
+
);
|
|
460
|
+
};
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
#### createEventEmitter
|
|
464
|
+
|
|
465
|
+
Create event emitter without payload:
|
|
466
|
+
|
|
467
|
+
```tsx
|
|
468
|
+
import * as React from 'react';
|
|
469
|
+
import {
|
|
470
|
+
createState,
|
|
471
|
+
debounceTime,
|
|
472
|
+
filter,
|
|
473
|
+
fromPromise,
|
|
474
|
+
type Observable,
|
|
475
|
+
switchMap,
|
|
476
|
+
} from 'synstate';
|
|
477
|
+
import { Result } from 'ts-data-forge';
|
|
478
|
+
|
|
479
|
+
const [searchState, setSearchState] = createState('');
|
|
480
|
+
|
|
481
|
+
// Advanced reactive pipeline (optional feature)
|
|
482
|
+
const searchResults$: Observable<
|
|
483
|
+
Result<readonly Readonly<{ id: string; name: string }>[], unknown>
|
|
484
|
+
> = searchState
|
|
485
|
+
.pipe(debounceTime(300))
|
|
486
|
+
.pipe(filter((query) => query.length > 2))
|
|
487
|
+
.pipe(
|
|
488
|
+
switchMap((query) =>
|
|
489
|
+
fromPromise(
|
|
490
|
+
fetch(`/api/search?q=${query}`).then(
|
|
491
|
+
(r) =>
|
|
492
|
+
r.json() as Promise<
|
|
493
|
+
readonly Readonly<{ id: string; name: string }>[]
|
|
494
|
+
>,
|
|
495
|
+
),
|
|
496
|
+
),
|
|
497
|
+
),
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
const SearchBox = (): React.JSX.Element => {
|
|
501
|
+
const [results, setResults] = React.useState<
|
|
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
|
+
}, []);
|
|
516
|
+
|
|
517
|
+
return (
|
|
518
|
+
<div>
|
|
519
|
+
<input
|
|
520
|
+
placeholder={'Search...'}
|
|
521
|
+
onChange={(e) => {
|
|
522
|
+
setSearchState(e.target.value);
|
|
523
|
+
}}
|
|
524
|
+
/>
|
|
525
|
+
<ul>
|
|
526
|
+
{results.map((item) => (
|
|
527
|
+
<li key={item.id}>{item.name}</li>
|
|
528
|
+
))}
|
|
529
|
+
</ul>
|
|
530
|
+
</div>
|
|
531
|
+
);
|
|
532
|
+
};
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Advanced Features (Optional)
|
|
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)
|
|
648
|
+
|
|
649
|
+
```tsx
|
|
650
|
+
import { createReducer } from 'synstate';
|
|
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
|
+
}, []);
|
|
686
|
+
|
|
687
|
+
return (
|
|
688
|
+
<div>
|
|
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
|
+
);
|
|
790
|
+
|
|
791
|
+
const SearchBox = (): React.JSX.Element => {
|
|
792
|
+
const [results, setResults] = React.useState<
|
|
793
|
+
readonly Readonly<{ id: string; name: string }>[]
|
|
794
|
+
>([]);
|
|
795
|
+
|
|
796
|
+
React.useEffect(() => {
|
|
797
|
+
const sub = searchResults$.subscribe((result) => {
|
|
798
|
+
if (Result.isOk(result)) {
|
|
799
|
+
setResults(result.value);
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
return () => {
|
|
804
|
+
sub.unsubscribe();
|
|
805
|
+
};
|
|
806
|
+
}, []);
|
|
807
|
+
|
|
808
|
+
return (
|
|
809
|
+
<div>
|
|
810
|
+
<input
|
|
811
|
+
placeholder={'Search...'}
|
|
812
|
+
onChange={(e) => {
|
|
813
|
+
setSearchState(e.target.value);
|
|
814
|
+
}}
|
|
815
|
+
/>
|
|
816
|
+
<ul>
|
|
817
|
+
{results.map((item) => (
|
|
818
|
+
<li key={item.id}>{item.name}</li>
|
|
819
|
+
))}
|
|
820
|
+
</ul>
|
|
821
|
+
</div>
|
|
822
|
+
);
|
|
823
|
+
};
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
## Why SyncFlow?
|
|
827
|
+
|
|
828
|
+
### Simple State Management, Not Complex Reactive Programming
|
|
829
|
+
|
|
830
|
+
Unlike RxJS, which can make code harder to read with many operators and complex streams, SyncFlow focuses on **simple, readable state management and event handling**. Most applications only need `createState`, `createReducer`, and `createValueEmitter` - clean, straightforward APIs that developers understand immediately.
|
|
831
|
+
|
|
832
|
+
**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
|
+
|
|
834
|
+
### Key Differences from RxJS
|
|
835
|
+
|
|
836
|
+
- **Focus on State & Events**: Designed for state management and event-driven architecture
|
|
837
|
+
- **Simpler API**: Most use cases covered by `createState`, `createReducer`, and `createValueEmitter`
|
|
838
|
+
- **Better Readability**: No need for complex operator chains in everyday code
|
|
839
|
+
- **Optional Complexity**: Advanced features available when needed
|
|
840
|
+
|
|
841
|
+
### Use Cases
|
|
842
|
+
|
|
843
|
+
**Use SyncFlow when you need:**
|
|
844
|
+
|
|
845
|
+
- ✅ Global state management across components
|
|
846
|
+
- ✅ Event-driven communication between components
|
|
847
|
+
- ✅ Type-safe event emitters
|
|
848
|
+
- ✅ Redux-like state with reducers
|
|
849
|
+
- ✅ Simple reactive patterns (debounce, throttle, etc.)
|
|
850
|
+
|
|
851
|
+
**Consider other solutions when:**
|
|
852
|
+
|
|
853
|
+
- ❌ You need complex stream processing (use RxJS)
|
|
854
|
+
- ❌ Your app is simple enough for React Context alone
|
|
855
|
+
|
|
856
|
+
## Type Safety
|
|
857
|
+
|
|
858
|
+
SyncFlow maintains full type information:
|
|
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
|
+
```
|
|
871
|
+
|
|
872
|
+
## License
|
|
873
|
+
|
|
874
|
+
This project is licensed under the [Apache License 2.0](./LICENSE).
|
|
875
|
+
|
|
876
|
+
## Repository
|
|
877
|
+
|
|
878
|
+
<https://github.com/noshiro-pf/synstate>
|