synstate 0.1.1 → 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/README.md +184 -218
- package/dist/core/combine/combine.d.mts +1 -1
- package/dist/core/combine/combine.mjs +2 -2
- package/dist/core/combine/combine.mjs.map +1 -1
- package/dist/core/create/source.d.mts +1 -1
- package/dist/core/create/source.mjs +2 -2
- package/dist/core/create/source.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/index.mjs +3 -3
- package/dist/core/operators/map-with-index.d.mts +0 -13
- package/dist/core/operators/map-with-index.d.mts.map +1 -1
- package/dist/core/operators/map-with-index.mjs +2 -19
- package/dist/core/operators/map-with-index.mjs.map +1 -1
- package/dist/core/operators/merge-map.d.mts +1 -1
- package/dist/core/operators/merge-map.mjs +1 -1
- package/dist/core/operators/skip-if-no-change.d.mts +1 -1
- package/dist/core/operators/skip-if-no-change.mjs +2 -2
- package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
- package/dist/core/operators/skip-while.d.mts +0 -1
- package/dist/core/operators/skip-while.d.mts.map +1 -1
- package/dist/core/operators/skip-while.mjs +2 -5
- package/dist/core/operators/skip-while.mjs.map +1 -1
- package/dist/core/operators/take-while.d.mts +0 -1
- package/dist/core/operators/take-while.d.mts.map +1 -1
- package/dist/core/operators/take-while.mjs +1 -3
- package/dist/core/operators/take-while.mjs.map +1 -1
- package/dist/core/operators/with-buffered-from.d.mts +4 -0
- package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
- package/dist/core/operators/with-buffered-from.mjs +5 -1
- package/dist/core/operators/with-buffered-from.mjs.map +1 -1
- package/dist/core/operators/with-current-value-from.d.mts +4 -0
- package/dist/core/operators/with-current-value-from.d.mts.map +1 -1
- package/dist/core/operators/with-current-value-from.mjs +5 -1
- package/dist/core/operators/with-current-value-from.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/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/package.json +1 -1
- package/src/core/combine/combine.mts +2 -2
- package/src/core/create/source.mts +2 -2
- package/src/core/index.mts +1 -0
- package/src/core/operators/map-with-index.mts +3 -62
- package/src/core/operators/merge-map.mts +1 -1
- package/src/core/operators/skip-if-no-change.mts +2 -2
- package/src/core/operators/skip-while.mts +1 -16
- package/src/core/operators/take-while.mts +2 -8
- package/src/core/operators/with-buffered-from.mts +5 -1
- package/src/core/operators/with-current-value-from.mts +5 -1
- 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/README.md
CHANGED
|
@@ -13,18 +13,18 @@
|
|
|
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
17
|
|
|
18
18
|
## Features
|
|
19
19
|
|
|
20
20
|
- 🎯 **Simple State Management**: Easy-to-use `createState` and `createReducer` for global state
|
|
21
21
|
- 📡 **Event System**: Built-in `createValueEmitter`, `createEventEmitter` for event-driven architecture
|
|
22
|
-
- 🔄 **Reactive Updates**: Automatic propagation of state changes to subscribers
|
|
22
|
+
- 🔄 **Reactive Updates**: Automatic propagation of state changes to all subscribers
|
|
23
23
|
- 🎨 **Type-Safe**: Full TypeScript support with precise type inference
|
|
24
24
|
- 🚀 **Lightweight**: Minimal bundle size with only one external runtime dependency ([ts-data-forge](https://www.npmjs.com/package/ts-data-forge))
|
|
25
25
|
- ⚡ **High Performance**: Optimized for fast state updates and minimal re-renders
|
|
26
26
|
- 🌐 **Framework Agnostic**: Works with React, Vue, Svelte, or vanilla JavaScript
|
|
27
|
-
- 🔧 **
|
|
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`)
|
|
28
28
|
|
|
29
29
|
## Documentation
|
|
30
30
|
|
|
@@ -52,12 +52,13 @@ pnpm add synstate
|
|
|
52
52
|
|
|
53
53
|
```tsx
|
|
54
54
|
// Create a reactive state
|
|
55
|
-
const [state, setState, { updateState }] =
|
|
55
|
+
const [state, setState, { updateState, resetState, getSnapshot }] =
|
|
56
|
+
createState(0);
|
|
56
57
|
|
|
57
58
|
const mut_history: number[] = [];
|
|
58
59
|
|
|
59
60
|
// Subscribe to changes (in React components, Vue watchers, etc.)
|
|
60
|
-
state.subscribe((count
|
|
61
|
+
state.subscribe((count) => {
|
|
61
62
|
mut_history.push(count);
|
|
62
63
|
});
|
|
63
64
|
|
|
@@ -68,9 +69,15 @@ setState(1);
|
|
|
68
69
|
|
|
69
70
|
assert.deepStrictEqual(mut_history, [0, 1]);
|
|
70
71
|
|
|
71
|
-
updateState((prev
|
|
72
|
+
updateState((prev) => prev + 2);
|
|
73
|
+
|
|
74
|
+
assert.deepStrictEqual(mut_history, [0, 1, 3]);
|
|
75
|
+
|
|
76
|
+
assert.isTrue(getSnapshot() === 3);
|
|
72
77
|
|
|
73
|
-
|
|
78
|
+
resetState();
|
|
79
|
+
|
|
80
|
+
assert.isTrue(getSnapshot() === 0);
|
|
74
81
|
```
|
|
75
82
|
|
|
76
83
|
### With React
|
|
@@ -117,59 +124,115 @@ const UserProfile = (): React.JSX.Element => {
|
|
|
117
124
|
};
|
|
118
125
|
```
|
|
119
126
|
|
|
120
|
-
|
|
127
|
+
If you're using React v18 or later:
|
|
121
128
|
|
|
122
|
-
|
|
129
|
+
```tsx
|
|
130
|
+
import * as React from 'react';
|
|
131
|
+
import { createState } from 'synstate';
|
|
123
132
|
|
|
124
|
-
|
|
133
|
+
const [userState, setUserState] = createState({
|
|
134
|
+
name: '',
|
|
135
|
+
email: '',
|
|
136
|
+
});
|
|
125
137
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
138
|
+
const UserProfile = (): React.JSX.Element => {
|
|
139
|
+
const user = React.useSyncExternalStore(
|
|
140
|
+
(onStoreChange: () => void) => {
|
|
141
|
+
const { unsubscribe } = userState.subscribe(onStoreChange);
|
|
129
142
|
|
|
130
|
-
|
|
143
|
+
return unsubscribe;
|
|
144
|
+
},
|
|
145
|
+
() => userState.getSnapshot().value,
|
|
146
|
+
);
|
|
131
147
|
|
|
132
|
-
|
|
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
|
+
```
|
|
133
168
|
|
|
134
|
-
|
|
135
|
-
- **`createEventEmitter`**: Create event emitters without payload
|
|
169
|
+
You can write the equivalent code more concisely using synstate-react-hooks:
|
|
136
170
|
|
|
137
|
-
|
|
171
|
+
```bash
|
|
172
|
+
npm add synstate-react-hooks
|
|
173
|
+
```
|
|
138
174
|
|
|
139
|
-
|
|
175
|
+
```tsx
|
|
176
|
+
import type * as React from 'react';
|
|
177
|
+
import { createState } from 'synstate-react-hooks';
|
|
140
178
|
|
|
141
|
-
|
|
179
|
+
const [useUserState, setUserState] = createState({
|
|
180
|
+
name: '',
|
|
181
|
+
email: '',
|
|
182
|
+
});
|
|
142
183
|
|
|
143
|
-
|
|
184
|
+
const UserProfile = (): React.JSX.Element => {
|
|
185
|
+
const user = useUserState();
|
|
144
186
|
|
|
145
|
-
|
|
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
|
+
```
|
|
146
207
|
|
|
147
|
-
|
|
208
|
+
See also the [synstate-react-hooks README](../synstate-react-hooks/README.md).
|
|
148
209
|
|
|
149
|
-
|
|
210
|
+
## Core Concepts
|
|
150
211
|
|
|
151
|
-
|
|
212
|
+
### State Management
|
|
152
213
|
|
|
153
|
-
|
|
214
|
+
SynState provides simple, intuitive APIs for managing application state:
|
|
154
215
|
|
|
155
|
-
Create state with
|
|
216
|
+
- **`createState`**: Create state with getter/setter
|
|
217
|
+
- **`createReducer`**: Create state by reducer and initial value
|
|
218
|
+
- **`createBooleanState`**: Specialized state for boolean values
|
|
156
219
|
|
|
157
220
|
### Event System
|
|
158
221
|
|
|
159
|
-
|
|
222
|
+
Built-in event emitter for event-driven patterns:
|
|
160
223
|
|
|
161
|
-
Create type-safe event
|
|
224
|
+
- **`createValueEmitter`**: Create type-safe event emitters
|
|
225
|
+
- **`createEventEmitter`**: Create event emitters without payload
|
|
162
226
|
|
|
163
|
-
|
|
227
|
+
### Observable (Optional Advanced Feature)
|
|
164
228
|
|
|
165
|
-
|
|
166
|
-
-->
|
|
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`.
|
|
167
230
|
|
|
168
|
-
|
|
231
|
+
## API Reference
|
|
169
232
|
|
|
170
233
|
For complex scenarios, SynState provides observable-based APIs:
|
|
171
234
|
|
|
172
|
-
|
|
235
|
+
### Creation Functions
|
|
173
236
|
|
|
174
237
|
- `source<T>()`: Create a new observable source
|
|
175
238
|
- `of(value)`: Create observable from a single value
|
|
@@ -178,7 +241,7 @@ For complex scenarios, SynState provides observable-based APIs:
|
|
|
178
241
|
- `interval(ms)`: Emit values at intervals
|
|
179
242
|
- `timer(delay)`: Emit after delay
|
|
180
243
|
|
|
181
|
-
|
|
244
|
+
### Operators
|
|
182
245
|
|
|
183
246
|
- `filter(predicate)`: Filter values
|
|
184
247
|
- `map(fn)`: Transform values
|
|
@@ -188,7 +251,7 @@ For complex scenarios, SynState provides observable-based APIs:
|
|
|
188
251
|
- `skipIfNoChange()`: Skip duplicate values
|
|
189
252
|
- `takeUntil(notifier)`: Complete on notifier emission
|
|
190
253
|
|
|
191
|
-
|
|
254
|
+
### Combination
|
|
192
255
|
|
|
193
256
|
- `combine(observables)`: Combine latest values from multiple sources
|
|
194
257
|
- `merge(observables)`: Merge multiple streams
|
|
@@ -199,24 +262,16 @@ For complex scenarios, SynState provides observable-based APIs:
|
|
|
199
262
|
### Global Counter State (React)
|
|
200
263
|
|
|
201
264
|
```tsx
|
|
202
|
-
import * as React from 'react';
|
|
203
|
-
import { createState } from 'synstate';
|
|
265
|
+
import type * as React from 'react';
|
|
266
|
+
import { createState } from 'synstate-react-hooks';
|
|
204
267
|
|
|
205
268
|
// Create global state
|
|
206
|
-
export const [
|
|
269
|
+
export const [useCounterState, , { updateState, resetState, getSnapshot }] =
|
|
207
270
|
createState(0);
|
|
208
271
|
|
|
209
272
|
// Component 1
|
|
210
273
|
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
|
-
}, []);
|
|
274
|
+
const count = useCounterState();
|
|
220
275
|
|
|
221
276
|
return (
|
|
222
277
|
<div>
|
|
@@ -247,77 +302,17 @@ const ResetButton = (): React.JSX.Element => (
|
|
|
247
302
|
);
|
|
248
303
|
```
|
|
249
304
|
|
|
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
305
|
### Todo List with Reducer (React)
|
|
315
306
|
|
|
316
307
|
```tsx
|
|
317
308
|
import * as React from 'react';
|
|
318
|
-
import { createReducer } from 'synstate';
|
|
309
|
+
import { createReducer } from 'synstate-react-hooks';
|
|
319
310
|
|
|
320
|
-
type Todo = Readonly<{
|
|
311
|
+
type Todo = Readonly<{
|
|
312
|
+
id: number;
|
|
313
|
+
text: string;
|
|
314
|
+
done: boolean;
|
|
315
|
+
}>;
|
|
321
316
|
|
|
322
317
|
type Action = Readonly<
|
|
323
318
|
| { type: 'add'; text: string }
|
|
@@ -325,10 +320,9 @@ type Action = Readonly<
|
|
|
325
320
|
| { type: 'remove'; id: number }
|
|
326
321
|
>;
|
|
327
322
|
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
>((todos, action) => {
|
|
323
|
+
const initialTodos: readonly Todo[] = [];
|
|
324
|
+
|
|
325
|
+
const reducer = (todos: readonly Todo[], action: Action): readonly Todo[] => {
|
|
332
326
|
switch (action.type) {
|
|
333
327
|
case 'add':
|
|
334
328
|
return [
|
|
@@ -339,53 +333,66 @@ const [todoState, dispatch, getSnapshot] = createReducer<
|
|
|
339
333
|
done: false,
|
|
340
334
|
},
|
|
341
335
|
];
|
|
336
|
+
|
|
342
337
|
case 'toggle':
|
|
343
338
|
return todos.map((t) =>
|
|
344
339
|
t.id === action.id ? { ...t, done: !t.done } : t,
|
|
345
340
|
);
|
|
341
|
+
|
|
346
342
|
case 'remove':
|
|
347
343
|
return todos.filter((t) => t.id !== action.id);
|
|
348
344
|
}
|
|
349
|
-
}
|
|
345
|
+
};
|
|
350
346
|
|
|
351
|
-
const
|
|
352
|
-
|
|
347
|
+
const [useTodoState, dispatch] = createReducer<readonly Todo[], Action>(
|
|
348
|
+
reducer,
|
|
349
|
+
initialTodos,
|
|
350
|
+
);
|
|
353
351
|
|
|
354
|
-
|
|
355
|
-
|
|
352
|
+
const addTodo = (): void => {
|
|
353
|
+
dispatch({
|
|
354
|
+
type: 'add',
|
|
355
|
+
text: 'New Todo',
|
|
356
|
+
});
|
|
357
|
+
};
|
|
356
358
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
+
);
|
|
361
381
|
|
|
362
382
|
return (
|
|
363
383
|
<div>
|
|
364
|
-
{
|
|
384
|
+
{todosWithHandler.map((todo) => (
|
|
365
385
|
<div key={todo.id}>
|
|
366
386
|
<input
|
|
367
387
|
checked={todo.done}
|
|
368
388
|
type={'checkbox'}
|
|
369
|
-
onChange={
|
|
370
|
-
dispatch({
|
|
371
|
-
type: 'toggle',
|
|
372
|
-
id: todo.id,
|
|
373
|
-
});
|
|
374
|
-
}}
|
|
389
|
+
onChange={todo.onToggle}
|
|
375
390
|
/>
|
|
376
391
|
<span>{todo.text}</span>
|
|
392
|
+
<button onClick={todo.onRemove}>{'Remove'}</button>
|
|
377
393
|
</div>
|
|
378
394
|
))}
|
|
379
|
-
<button
|
|
380
|
-
onClick={() => {
|
|
381
|
-
dispatch({
|
|
382
|
-
type: 'add',
|
|
383
|
-
text: 'New Todo',
|
|
384
|
-
});
|
|
385
|
-
}}
|
|
386
|
-
>
|
|
387
|
-
{'Add Todo'}
|
|
388
|
-
</button>
|
|
395
|
+
<button onClick={addTodo}>{'Add Todo'}</button>
|
|
389
396
|
</div>
|
|
390
397
|
);
|
|
391
398
|
};
|
|
@@ -395,35 +402,19 @@ const TodoList = (): React.JSX.Element => {
|
|
|
395
402
|
|
|
396
403
|
```tsx
|
|
397
404
|
import * as React from 'react';
|
|
398
|
-
import { createBooleanState } from 'synstate';
|
|
405
|
+
import { createBooleanState } from 'synstate-react-hooks';
|
|
399
406
|
|
|
400
|
-
export const [
|
|
407
|
+
export const [useDarkModeState, { toggle: toggleDarkMode }] =
|
|
401
408
|
createBooleanState(false);
|
|
402
409
|
|
|
403
410
|
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
|
-
}, []);
|
|
411
|
+
const isDark = useDarkModeState();
|
|
413
412
|
|
|
414
413
|
React.useEffect(() => {
|
|
415
414
|
document.body.className = isDark ? 'dark' : 'light';
|
|
416
415
|
}, [isDark]);
|
|
417
416
|
|
|
418
|
-
return
|
|
419
|
-
<button
|
|
420
|
-
onClick={() => {
|
|
421
|
-
toggle();
|
|
422
|
-
}}
|
|
423
|
-
>
|
|
424
|
-
{isDark ? '🌙' : '☀️'}
|
|
425
|
-
</button>
|
|
426
|
-
);
|
|
417
|
+
return <button onClick={toggleDarkMode}>{isDark ? '🌙' : '☀️'}</button>;
|
|
427
418
|
};
|
|
428
419
|
```
|
|
429
420
|
|
|
@@ -431,30 +422,20 @@ const ThemeToggle = (): React.JSX.Element => {
|
|
|
431
422
|
|
|
432
423
|
```tsx
|
|
433
424
|
import * as React from 'react';
|
|
434
|
-
import {
|
|
435
|
-
|
|
436
|
-
// Events
|
|
437
|
-
const [onItemAdded$, emitItemAdded] = createValueEmitter<string>();
|
|
438
|
-
|
|
439
|
-
const [onClearAll$, emitClearAll] = createEventEmitter();
|
|
425
|
+
import { createState } from 'synstate-react-hooks';
|
|
440
426
|
|
|
441
427
|
// State
|
|
442
|
-
const [
|
|
443
|
-
readonly string[]
|
|
444
|
-
>([]);
|
|
428
|
+
const [useItemsState, _, { updateState, resetState: resetItemsState }] =
|
|
429
|
+
createState<readonly string[]>([]);
|
|
445
430
|
|
|
446
431
|
// Setup event handlers
|
|
447
|
-
|
|
432
|
+
const addItem = (item: string): void => {
|
|
448
433
|
updateState((items: readonly string[]) => [...items, item]);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
onClearAll$.subscribe(() => {
|
|
452
|
-
setItemsState([]);
|
|
453
|
-
});
|
|
434
|
+
};
|
|
454
435
|
|
|
455
436
|
// Component 1: Add items
|
|
456
437
|
const ItemInput = (): React.JSX.Element => {
|
|
457
|
-
const [input, setInput] = React.useState('');
|
|
438
|
+
const [input, setInput] = React.useState<string>('');
|
|
458
439
|
|
|
459
440
|
return (
|
|
460
441
|
<div>
|
|
@@ -466,7 +447,7 @@ const ItemInput = (): React.JSX.Element => {
|
|
|
466
447
|
/>
|
|
467
448
|
<button
|
|
468
449
|
onClick={() => {
|
|
469
|
-
|
|
450
|
+
addItem(input);
|
|
470
451
|
|
|
471
452
|
setInput('');
|
|
472
453
|
}}
|
|
@@ -479,15 +460,7 @@ const ItemInput = (): React.JSX.Element => {
|
|
|
479
460
|
|
|
480
461
|
// Component 2: Display items
|
|
481
462
|
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
|
-
}, []);
|
|
463
|
+
const items = useItemsState();
|
|
491
464
|
|
|
492
465
|
return (
|
|
493
466
|
<div>
|
|
@@ -496,7 +469,7 @@ const ItemList = (): React.JSX.Element => {
|
|
|
496
469
|
<li key={i}>{item}</li>
|
|
497
470
|
))}
|
|
498
471
|
</ul>
|
|
499
|
-
<button onClick={
|
|
472
|
+
<button onClick={resetItemsState}>{'Clear All'}</button>
|
|
500
473
|
</div>
|
|
501
474
|
);
|
|
502
475
|
};
|
|
@@ -507,22 +480,25 @@ const ItemList = (): React.JSX.Element => {
|
|
|
507
480
|
### Advanced: Search with Debounce
|
|
508
481
|
|
|
509
482
|
```tsx
|
|
510
|
-
import * as React from 'react';
|
|
483
|
+
import type * as React from 'react';
|
|
511
484
|
import {
|
|
512
485
|
createState,
|
|
513
486
|
debounceTime,
|
|
514
487
|
filter,
|
|
515
488
|
fromPromise,
|
|
516
|
-
type
|
|
489
|
+
type InitializedObservable,
|
|
490
|
+
map,
|
|
517
491
|
switchMap,
|
|
492
|
+
withInitialValue,
|
|
518
493
|
} from 'synstate';
|
|
494
|
+
import { useObservableValue } from 'synstate-react-hooks';
|
|
519
495
|
import { Result } from 'ts-data-forge';
|
|
520
496
|
|
|
521
497
|
const [searchState, setSearchState] = createState('');
|
|
522
498
|
|
|
523
|
-
// Advanced reactive pipeline
|
|
524
|
-
const searchResults$:
|
|
525
|
-
|
|
499
|
+
// Advanced reactive pipeline with debounce and filtering
|
|
500
|
+
const searchResults$: InitializedObservable<
|
|
501
|
+
readonly Readonly<{ id: string; name: string }>[]
|
|
526
502
|
> = searchState
|
|
527
503
|
.pipe(debounceTime(300))
|
|
528
504
|
.pipe(filter((query) => query.length > 2))
|
|
@@ -537,24 +513,13 @@ const searchResults$: Observable<
|
|
|
537
513
|
),
|
|
538
514
|
),
|
|
539
515
|
),
|
|
540
|
-
)
|
|
516
|
+
)
|
|
517
|
+
.pipe(filter((res) => Result.isOk(res)))
|
|
518
|
+
.pipe(map((res) => Result.unwrapOk(res)))
|
|
519
|
+
.pipe(withInitialValue([]));
|
|
541
520
|
|
|
542
521
|
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
|
-
}, []);
|
|
522
|
+
const searchResults = useObservableValue(searchResults$);
|
|
558
523
|
|
|
559
524
|
return (
|
|
560
525
|
<div>
|
|
@@ -565,7 +530,7 @@ const SearchBox = (): React.JSX.Element => {
|
|
|
565
530
|
}}
|
|
566
531
|
/>
|
|
567
532
|
<ul>
|
|
568
|
-
{
|
|
533
|
+
{searchResults.map((item) => (
|
|
569
534
|
<li key={item.id}>{item.name}</li>
|
|
570
535
|
))}
|
|
571
536
|
</ul>
|
|
@@ -613,16 +578,17 @@ const DataTable = (): React.JSX.Element => (
|
|
|
613
578
|
|
|
614
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.
|
|
615
580
|
|
|
616
|
-
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 `
|
|
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.
|
|
617
582
|
|
|
618
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.
|
|
619
584
|
|
|
620
585
|
### Key Differences from RxJS
|
|
621
586
|
|
|
622
|
-
- **Focus on State
|
|
623
|
-
- **
|
|
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`
|
|
624
590
|
- **Better Readability**: No need for complex operator chains in everyday code
|
|
625
|
-
- **Optional Complexity**: Advanced features available when needed
|
|
591
|
+
- **Optional Complexity**: Advanced features available to manipulate Observables when needed
|
|
626
592
|
|
|
627
593
|
### Use Cases
|
|
628
594
|
|
|
@@ -636,7 +602,7 @@ Under the hood, SynState is built on Observable patterns similar to those provid
|
|
|
636
602
|
|
|
637
603
|
**Consider other solutions when:**
|
|
638
604
|
|
|
639
|
-
- ❌ You need
|
|
605
|
+
- ❌ You need state in a React component (use React hooks `useState`, `useReducer`)
|
|
640
606
|
- ❌ Your app is simple enough for React Context alone
|
|
641
607
|
|
|
642
608
|
## Type Safety
|
|
@@ -58,7 +58,7 @@ import { type CombineObservableRefined, type Observable } from '../types/index.m
|
|
|
58
58
|
*/
|
|
59
59
|
export declare const combine: <const OS extends NonEmptyArray<Observable<unknown>>>(parents: OS) => CombineObservableRefined<OS>;
|
|
60
60
|
/**
|
|
61
|
-
* Alias for `combine
|
|
61
|
+
* Alias for `combine`.
|
|
62
62
|
* @see combine
|
|
63
63
|
*/
|
|
64
64
|
export declare const combineLatest: <const OS extends NonEmptyArray<Observable<unknown>>>(parents: OS) => CombineObservableRefined<OS>;
|
|
@@ -66,10 +66,10 @@ const combine = (parents) =>
|
|
|
66
66
|
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
67
67
|
new CombineObservableClass(parents);
|
|
68
68
|
/**
|
|
69
|
-
* Alias for `combine
|
|
69
|
+
* Alias for `combine`.
|
|
70
70
|
* @see combine
|
|
71
71
|
*/
|
|
72
|
-
const combineLatest = combine;
|
|
72
|
+
const combineLatest = combine;
|
|
73
73
|
class CombineObservableClass extends SyncChildObservableClass {
|
|
74
74
|
constructor(parents) {
|
|
75
75
|
const parentsValues = parents.map((p) => p.getSnapshot());
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"combine.mjs","sources":["../../../src/core/combine/combine.mts"],"sourcesContent":[null],"names":[],"mappings":";;;;;;;AAgBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDG;AACI,MAAM,OAAO,GAAG,CACrB,OAAW;AAEX;AACA,IAAI,sBAAsB,CACxB,OAAO;AAGX;;;AAGG;AACI,MAAM,aAAa,GAAG
|
|
1
|
+
{"version":3,"file":"combine.mjs","sources":["../../../src/core/combine/combine.mts"],"sourcesContent":[null],"names":[],"mappings":";;;;;;;AAgBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDG;AACI,MAAM,OAAO,GAAG,CACrB,OAAW;AAEX;AACA,IAAI,sBAAsB,CACxB,OAAO;AAGX;;;AAGG;AACI,MAAM,aAAa,GAAG;AAE7B,MAAM,sBACJ,SAAQ,wBAA8B,CAAA;AAGtC,IAAA,WAAA,CAAY,OAAgB,EAAA;AAC1B,QAAA,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;AAEzD,QAAA,KAAK,CAAC;YACJ,OAAO;YACP,YAAY,EAAE,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM;kBAC7C,QAAQ,CAAC,IAAI;;AAEX,gBAAA,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,CAAM;kBAE7C,QAAQ,CAAC,IAAI;AAClB,SAAA,CAAC;;AAGK,IAAA,SAAS,CAAC,aAA4B,EAAA;AAC7C,QAAA,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,aAAa,KAAK,aAAa,CAAC;AAAE,YAAA,OAAO;AAEzE,QAAA,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QAE7D,IAAI,YAAY,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE;AACvC,YAAA,MAAM,SAAS;;AAEb,YAAA,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,CAAM;AAE5C,YAAA,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,aAAa,CAAC;;;AAG3C;AAED;IACE;AACE,QAAA,MAAM,EAAE,GAAkB,MAAM,EAAK;AAErC,QAAA,MAAM,EAAE,GAAkB,MAAM,EAAK;QAE1B,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC;;IAO7B;AACE,QAAA,MAAM,EAAE,GAA6B,MAAM,EAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;AAE1E,QAAA,MAAM,EAAE,GAAkB,MAAM,EAAK;QAE1B,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC;;IAO7B;AACE,QAAA,MAAM,EAAE,GAA6B,MAAM,EAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;AAE1E,QAAA,MAAM,EAAE,GAA6B,MAAM,EAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;QAE/D,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC;;;AAM7B,IAAA,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAE/B,IAAA,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;IAE1B,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC;IAEf,OAAO,CAAC;AAClB,QAAA,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;AAC5B,QAAA,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;AAC7B,KAAA;AAQH;;;;"}
|