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.
Files changed (122) hide show
  1. package/README.md +184 -218
  2. package/dist/core/combine/combine.d.mts +1 -1
  3. package/dist/core/combine/combine.mjs +2 -2
  4. package/dist/core/combine/combine.mjs.map +1 -1
  5. package/dist/core/create/source.d.mts +1 -1
  6. package/dist/core/create/source.mjs +2 -2
  7. package/dist/core/create/source.mjs.map +1 -1
  8. package/dist/core/index.d.mts +1 -0
  9. package/dist/core/index.d.mts.map +1 -1
  10. package/dist/core/index.mjs +15 -3
  11. package/dist/core/index.mjs.map +1 -1
  12. package/dist/core/operators/index.mjs +3 -3
  13. package/dist/core/operators/map-with-index.d.mts +0 -13
  14. package/dist/core/operators/map-with-index.d.mts.map +1 -1
  15. package/dist/core/operators/map-with-index.mjs +2 -19
  16. package/dist/core/operators/map-with-index.mjs.map +1 -1
  17. package/dist/core/operators/merge-map.d.mts +1 -1
  18. package/dist/core/operators/merge-map.mjs +1 -1
  19. package/dist/core/operators/skip-if-no-change.d.mts +1 -1
  20. package/dist/core/operators/skip-if-no-change.mjs +2 -2
  21. package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
  22. package/dist/core/operators/skip-while.d.mts +0 -1
  23. package/dist/core/operators/skip-while.d.mts.map +1 -1
  24. package/dist/core/operators/skip-while.mjs +2 -5
  25. package/dist/core/operators/skip-while.mjs.map +1 -1
  26. package/dist/core/operators/take-while.d.mts +0 -1
  27. package/dist/core/operators/take-while.d.mts.map +1 -1
  28. package/dist/core/operators/take-while.mjs +1 -3
  29. package/dist/core/operators/take-while.mjs.map +1 -1
  30. package/dist/core/operators/with-buffered-from.d.mts +4 -0
  31. package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
  32. package/dist/core/operators/with-buffered-from.mjs +5 -1
  33. package/dist/core/operators/with-buffered-from.mjs.map +1 -1
  34. package/dist/core/operators/with-current-value-from.d.mts +4 -0
  35. package/dist/core/operators/with-current-value-from.d.mts.map +1 -1
  36. package/dist/core/operators/with-current-value-from.mjs +5 -1
  37. package/dist/core/operators/with-current-value-from.mjs.map +1 -1
  38. package/dist/core/predefined/index.d.mts +2 -0
  39. package/dist/core/predefined/index.d.mts.map +1 -0
  40. package/dist/core/predefined/index.mjs +13 -0
  41. package/dist/core/predefined/index.mjs.map +1 -0
  42. package/dist/core/predefined/operators/attach-index.d.mts +8 -0
  43. package/dist/core/predefined/operators/attach-index.d.mts.map +1 -0
  44. package/dist/core/predefined/operators/attach-index.mjs +13 -0
  45. package/dist/core/predefined/operators/attach-index.mjs.map +1 -0
  46. package/dist/core/predefined/operators/index.d.mts +13 -0
  47. package/dist/core/predefined/operators/index.d.mts.map +1 -0
  48. package/dist/core/predefined/operators/index.mjs +13 -0
  49. package/dist/core/predefined/operators/index.mjs.map +1 -0
  50. package/dist/core/predefined/operators/map-optional.d.mts +4 -0
  51. package/dist/core/predefined/operators/map-optional.d.mts.map +1 -0
  52. package/dist/core/predefined/operators/map-optional.mjs +7 -0
  53. package/dist/core/predefined/operators/map-optional.mjs.map +1 -0
  54. package/dist/core/predefined/operators/map-result-err.d.mts +4 -0
  55. package/dist/core/predefined/operators/map-result-err.d.mts.map +1 -0
  56. package/dist/core/predefined/operators/map-result-err.mjs +7 -0
  57. package/dist/core/predefined/operators/map-result-err.mjs.map +1 -0
  58. package/dist/core/predefined/operators/map-result-ok.d.mts +4 -0
  59. package/dist/core/predefined/operators/map-result-ok.d.mts.map +1 -0
  60. package/dist/core/predefined/operators/map-result-ok.mjs +7 -0
  61. package/dist/core/predefined/operators/map-result-ok.mjs.map +1 -0
  62. package/dist/core/predefined/operators/map-to.d.mts +3 -0
  63. package/dist/core/predefined/operators/map-to.d.mts.map +1 -0
  64. package/dist/core/predefined/operators/map-to.mjs +6 -0
  65. package/dist/core/predefined/operators/map-to.mjs.map +1 -0
  66. package/dist/core/predefined/operators/map.d.mts +3 -0
  67. package/dist/core/predefined/operators/map.d.mts.map +1 -0
  68. package/dist/core/predefined/operators/map.mjs +8 -0
  69. package/dist/core/predefined/operators/map.mjs.map +1 -0
  70. package/dist/core/predefined/operators/pluck.d.mts +8 -0
  71. package/dist/core/predefined/operators/pluck.d.mts.map +1 -0
  72. package/dist/core/predefined/operators/pluck.mjs +11 -0
  73. package/dist/core/predefined/operators/pluck.mjs.map +1 -0
  74. package/dist/core/predefined/operators/skip.d.mts +3 -0
  75. package/dist/core/predefined/operators/skip.d.mts.map +1 -0
  76. package/dist/core/predefined/operators/skip.mjs +9 -0
  77. package/dist/core/predefined/operators/skip.mjs.map +1 -0
  78. package/dist/core/predefined/operators/take.d.mts +3 -0
  79. package/dist/core/predefined/operators/take.d.mts.map +1 -0
  80. package/dist/core/predefined/operators/take.mjs +8 -0
  81. package/dist/core/predefined/operators/take.mjs.map +1 -0
  82. package/dist/core/predefined/operators/unwrap-optional.d.mts +4 -0
  83. package/dist/core/predefined/operators/unwrap-optional.d.mts.map +1 -0
  84. package/dist/core/predefined/operators/unwrap-optional.mjs +9 -0
  85. package/dist/core/predefined/operators/unwrap-optional.mjs.map +1 -0
  86. package/dist/core/predefined/operators/unwrap-result-err.d.mts +4 -0
  87. package/dist/core/predefined/operators/unwrap-result-err.d.mts.map +1 -0
  88. package/dist/core/predefined/operators/unwrap-result-err.mjs +7 -0
  89. package/dist/core/predefined/operators/unwrap-result-err.mjs.map +1 -0
  90. package/dist/core/predefined/operators/unwrap-result-ok.d.mts +4 -0
  91. package/dist/core/predefined/operators/unwrap-result-ok.d.mts.map +1 -0
  92. package/dist/core/predefined/operators/unwrap-result-ok.mjs +9 -0
  93. package/dist/core/predefined/operators/unwrap-result-ok.mjs.map +1 -0
  94. package/dist/entry-point.mjs +15 -3
  95. package/dist/entry-point.mjs.map +1 -1
  96. package/dist/index.mjs +15 -3
  97. package/dist/index.mjs.map +1 -1
  98. package/package.json +1 -1
  99. package/src/core/combine/combine.mts +2 -2
  100. package/src/core/create/source.mts +2 -2
  101. package/src/core/index.mts +1 -0
  102. package/src/core/operators/map-with-index.mts +3 -62
  103. package/src/core/operators/merge-map.mts +1 -1
  104. package/src/core/operators/skip-if-no-change.mts +2 -2
  105. package/src/core/operators/skip-while.mts +1 -16
  106. package/src/core/operators/take-while.mts +2 -8
  107. package/src/core/operators/with-buffered-from.mts +5 -1
  108. package/src/core/operators/with-current-value-from.mts +5 -1
  109. package/src/core/predefined/index.mts +1 -0
  110. package/src/core/predefined/operators/attach-index.mts +13 -0
  111. package/src/core/predefined/operators/index.mts +12 -0
  112. package/src/core/predefined/operators/map-optional.mts +8 -0
  113. package/src/core/predefined/operators/map-result-err.mts +8 -0
  114. package/src/core/predefined/operators/map-result-ok.mts +8 -0
  115. package/src/core/predefined/operators/map-to.mts +5 -0
  116. package/src/core/predefined/operators/map.mts +5 -0
  117. package/src/core/predefined/operators/pluck.mts +12 -0
  118. package/src/core/predefined/operators/skip.mts +10 -0
  119. package/src/core/predefined/operators/take.mts +6 -0
  120. package/src/core/predefined/operators/unwrap-optional.mts +9 -0
  121. package/src/core/predefined/operators/unwrap-result-err.mts +8 -0
  122. 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
- - 🔧 **Flexible**: Simple state management with optional advanced Observable-based features (operators like `map`, `filter`, `debounceTime`, `throttleTime`, and combinators like `merge`, `combine`)
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 }] = createState(0);
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: number) => {
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: number) => prev + 1);
72
+ updateState((prev) => prev + 2);
73
+
74
+ assert.deepStrictEqual(mut_history, [0, 1, 3]);
75
+
76
+ assert.isTrue(getSnapshot() === 3);
72
77
 
73
- assert.deepStrictEqual(mut_history, [0, 1, 2]);
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
- ## Core Concepts
127
+ If you're using React v18 or later:
121
128
 
122
- ### State Management
129
+ ```tsx
130
+ import * as React from 'react';
131
+ import { createState } from 'synstate';
123
132
 
124
- SynState provides simple, intuitive APIs for managing application state:
133
+ const [userState, setUserState] = createState({
134
+ name: '',
135
+ email: '',
136
+ });
125
137
 
126
- - **`createState`**: Create mutable state with getter/setter
127
- - **`createReducer`**: Redux-style state management
128
- - **`createBooleanState`**: Specialized state for boolean values
138
+ const UserProfile = (): React.JSX.Element => {
139
+ const user = React.useSyncExternalStore(
140
+ (onStoreChange: () => void) => {
141
+ const { unsubscribe } = userState.subscribe(onStoreChange);
129
142
 
130
- ### Event System
143
+ return unsubscribe;
144
+ },
145
+ () => userState.getSnapshot().value,
146
+ );
131
147
 
132
- Built-in event emitter for event-driven patterns:
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
- - **`createValueEmitter`**: Create type-safe event emitters
135
- - **`createEventEmitter`**: Create event emitters without payload
169
+ You can write the equivalent code more concisely using synstate-react-hooks:
136
170
 
137
- ### Observable (Optional Advanced Feature)
171
+ ```bash
172
+ npm add synstate-react-hooks
173
+ ```
138
174
 
139
- For advanced use cases, you can use observables to build complex reactive data flows. However, most applications will only need `createState`, `createReducer`, and `createValueEmitter`.
175
+ ```tsx
176
+ import type * as React from 'react';
177
+ import { createState } from 'synstate-react-hooks';
140
178
 
141
- ## API Reference
179
+ const [useUserState, setUserState] = createState({
180
+ name: '',
181
+ email: '',
182
+ });
142
183
 
143
- <!-- ### State Management (Recommended)
184
+ const UserProfile = (): React.JSX.Element => {
185
+ const user = useUserState();
144
186
 
145
- #### createState
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
- Create reactive state with getter and setter.
208
+ See also the [synstate-react-hooks README](../synstate-react-hooks/README.md).
148
209
 
149
- #### createBooleanState
210
+ ## Core Concepts
150
211
 
151
- Specialized state for boolean values.
212
+ ### State Management
152
213
 
153
- #### createReducer
214
+ SynState provides simple, intuitive APIs for managing application state:
154
215
 
155
- Create state with reducer pattern (like Redux).
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
- #### createValueEmitter
222
+ Built-in event emitter for event-driven patterns:
160
223
 
161
- Create type-safe event emitter with payload.
224
+ - **`createValueEmitter`**: Create type-safe event emitters
225
+ - **`createEventEmitter`**: Create event emitters without payload
162
226
 
163
- #### createEventEmitter
227
+ ### Observable (Optional Advanced Feature)
164
228
 
165
- Create event emitter without payload.
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
- ### Advanced Features (Optional)
231
+ ## API Reference
169
232
 
170
233
  For complex scenarios, SynState provides observable-based APIs:
171
234
 
172
- #### Creation Functions
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
- #### Operators
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
- #### Combination
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 [counterState, , { updateState, resetState, getSnapshot }] =
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 [count, setCount] = React.useState(getSnapshot());
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<{ id: number; text: string; done: boolean }>;
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 [todoState, dispatch, getSnapshot] = createReducer<
329
- readonly Todo[],
330
- Action
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 TodoList = (): React.JSX.Element => {
352
- const [todos, setTodos] = React.useState(getSnapshot());
347
+ const [useTodoState, dispatch] = createReducer<readonly Todo[], Action>(
348
+ reducer,
349
+ initialTodos,
350
+ );
353
351
 
354
- React.useEffect(() => {
355
- const sub = todoState.subscribe(setTodos);
352
+ const addTodo = (): void => {
353
+ dispatch({
354
+ type: 'add',
355
+ text: 'New Todo',
356
+ });
357
+ };
356
358
 
357
- return () => {
358
- sub.unsubscribe();
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
- {todos.map((todo) => (
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 [darkModeState, { toggle, getSnapshot }] =
407
+ export const [useDarkModeState, { toggle: toggleDarkMode }] =
401
408
  createBooleanState(false);
402
409
 
403
410
  const ThemeToggle = (): React.JSX.Element => {
404
- const [isDark, setIsDark] = React.useState(getSnapshot());
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 { createEventEmitter, createState, createValueEmitter } from 'synstate';
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 [itemsState, setItemsState, { updateState, getSnapshot }] = createState<
443
- readonly string[]
444
- >([]);
428
+ const [useItemsState, _, { updateState, resetState: resetItemsState }] =
429
+ createState<readonly string[]>([]);
445
430
 
446
431
  // Setup event handlers
447
- onItemAdded$.subscribe((item) => {
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
- emitItemAdded(input);
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 [items, setItems] = React.useState(getSnapshot());
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={emitClearAll}>{'Clear All'}</button>
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 Observable,
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 (optional feature)
524
- const searchResults$: Observable<
525
- Result<readonly Readonly<{ id: string; name: string }>[], unknown>
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 [results, setResults] = React.useState<
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
- {results.map((item) => (
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 `createValueEmitter` - clean, straightforward APIs that developers understand immediately.
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 & Events**: Designed for state management and event-driven architecture
623
- - **Simpler API**: Most use cases covered by `createState`, `createReducer`, and `createValueEmitter`
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 complex stream processing (use RxJS)
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; // alias
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,QAAQ;AAErC,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;;;;"}
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;;;;"}