rx-tiny-flux 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/Readme.md +297 -0
- package/dist/rx-tiny-flux.esm.js +2175 -0
- package/examples/counter.js +132 -0
- package/jsconfig.json +8 -0
- package/package.json +40 -0
- package/rollup.config.js +43 -0
- package/src/actions.js +16 -0
- package/src/effects.js +33 -0
- package/src/index.js +8 -0
- package/src/reducers.js +67 -0
- package/src/rxjs.js +20 -0
- package/src/selectors.js +39 -0
- package/src/store.js +125 -0
- package/src/utils.js +25 -0
- package/src/zeppos.js +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Bernardo Baumblatt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/Readme.md
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# Rx-Tiny-Flux
|
|
2
|
+
|
|
3
|
+
`Rx-Tiny-Flux` is a lightweight, minimalist state management library for pure JavaScript projects, heavily inspired by the patterns of NgRx and Redux. It leverages the power of RxJS for reactive state management.
|
|
4
|
+
|
|
5
|
+
The primary dependencies are `rxjs` and `jsonpath`.
|
|
6
|
+
|
|
7
|
+
> **A Case Study on AI-Assisted Development:** This entire library was developed in just a few hours as a case study to explore the capabilities of **Gemini Code Assist**. It demonstrates how modern AI coding assistants can significantly accelerate the development process, from initial scaffolding to complex feature implementation and refinement.
|
|
8
|
+
|
|
9
|
+
## Core Concepts
|
|
10
|
+
|
|
11
|
+
The library is built around a few core concepts:
|
|
12
|
+
|
|
13
|
+
* **Store:** A single, centralized object that holds the application's state.
|
|
14
|
+
* **Actions:** Plain objects that describe events or "things that happened" in your application.
|
|
15
|
+
* **Reducers:** Pure functions that determine how the state changes in response to actions.
|
|
16
|
+
* **Effects:** Functions that handle side effects, such as API calls, which can dispatch new actions.
|
|
17
|
+
* **Selectors:** Pure functions used to query and derive data from the state.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Actions
|
|
22
|
+
|
|
23
|
+
Actions are the only source of information for the store. You dispatch them to trigger state changes. Use the `createAction` factory function to create them.
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
import { createAction } from 'rx-tiny-flux';
|
|
27
|
+
|
|
28
|
+
// An action creator for an event without a payload
|
|
29
|
+
const increment = createAction('[Counter] Increment');
|
|
30
|
+
|
|
31
|
+
// An action creator for an event with a payload
|
|
32
|
+
const add = createAction('[Counter] Add');
|
|
33
|
+
|
|
34
|
+
// Dispatching the actions
|
|
35
|
+
store.dispatch(increment()); // { type: '[Counter] Increment' }
|
|
36
|
+
store.dispatch(add(10)); // { type: '[Counter] Add', payload: 10 }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Reducers
|
|
42
|
+
|
|
43
|
+
Reducers specify how the application's state changes in response to actions. A reducer is a pure function that takes the previous state slice and an action, and returns the next state slice.
|
|
44
|
+
|
|
45
|
+
Use `createReducer` along with the `on` and `anyAction` helpers to define your reducer logic.
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
// Library imports
|
|
49
|
+
import { createReducer, on, anyAction } from 'rx-tiny-flux';
|
|
50
|
+
|
|
51
|
+
// Your application's action imports
|
|
52
|
+
import { increment, decrement, incrementSuccess } from './actions';
|
|
53
|
+
|
|
54
|
+
// This reducer manages the 'counter' slice of the state.
|
|
55
|
+
const counterReducer = createReducer(
|
|
56
|
+
// 1. The jsonpath to the state slice
|
|
57
|
+
'$.counter',
|
|
58
|
+
// 2. The initial state for this slice
|
|
59
|
+
{ value: 0, lastUpdate: null },
|
|
60
|
+
// 3. Handlers for specific actions
|
|
61
|
+
on(increment, incrementSuccess, (state) => ({
|
|
62
|
+
...state,
|
|
63
|
+
value: state.value + 1,
|
|
64
|
+
lastUpdate: new Date().toISOString(),
|
|
65
|
+
})),
|
|
66
|
+
on(decrement, (state) => ({
|
|
67
|
+
...state,
|
|
68
|
+
value: state.value - 1,
|
|
69
|
+
lastUpdate: new Date().toISOString(),
|
|
70
|
+
}))
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// This reducer manages the 'log' slice and reacts to ANY action.
|
|
74
|
+
const logReducer = createReducer(
|
|
75
|
+
'$.log',
|
|
76
|
+
[], // Initial state for this slice
|
|
77
|
+
// Using the `anyAction` token to create a handler that catches all actions.
|
|
78
|
+
on(anyAction, (state, action) => [...state, `Action: ${action.type} at ${new Date().toISOString()}`])
|
|
79
|
+
);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Effects
|
|
85
|
+
|
|
86
|
+
Effects are used to handle side effects, such as asynchronous operations (e.g., API calls). An effect listens for dispatched actions, performs some work, and can dispatch new actions as a result.
|
|
87
|
+
|
|
88
|
+
Use `createEffect` and the `ofType` operator to build effects.
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
// Core library imports
|
|
92
|
+
import { createEffect, ofType } from 'rx-tiny-flux';
|
|
93
|
+
// RxJS operators are imported from the secondary entry point
|
|
94
|
+
import { from, map, concatMap } from 'rx-tiny-flux/rxjs';
|
|
95
|
+
|
|
96
|
+
// Your application's action imports
|
|
97
|
+
import { incrementAsync, incrementSuccess } from './actions';
|
|
98
|
+
|
|
99
|
+
// A function that simulates an API call (e.g., fetch) which returns a Promise
|
|
100
|
+
const fakeApiCall = () => new Promise(resolve => setTimeout(() => resolve({ success: true }), 1000));
|
|
101
|
+
|
|
102
|
+
const incrementAsyncEffect = createEffect((actions$) =>
|
|
103
|
+
actions$.pipe(
|
|
104
|
+
// Listens only for the 'incrementAsync' action
|
|
105
|
+
ofType(incrementAsync),
|
|
106
|
+
// Use concatMap to handle the async operation.
|
|
107
|
+
// It waits for the inner Observable (from the Promise) to complete.
|
|
108
|
+
concatMap(() =>
|
|
109
|
+
from(fakeApiCall()).pipe( // `from` converts the Promise into an Observable
|
|
110
|
+
map(() => incrementSuccess()) // On success, map the result to a new action
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Using RxJS Operators
|
|
120
|
+
|
|
121
|
+
For use in restricted environments like **ZeppOS**, where direct dependencies are not allowed, `Rx-Tiny-Flux` re-exports a curated set of common RxJS operators and creation functions from a dedicated entry point: `rx-tiny-flux/rxjs`. This allows you to build complex effects without needing to import from `rxjs` directly in your application code.
|
|
122
|
+
|
|
123
|
+
**Available Exports:**
|
|
124
|
+
|
|
125
|
+
* **Creation Functions:** Used to create new Observables.
|
|
126
|
+
* `from`: Converts a Promise, an array, or an iterable into an Observable.
|
|
127
|
+
* `of`: Emits a variable number of arguments as a sequence and then completes.
|
|
128
|
+
* `defer`: Creates an Observable that, on subscription, calls an Observable factory to make a fresh Observable for each new Subscriber.
|
|
129
|
+
|
|
130
|
+
* **Higher-Order Mapping Operators:** Used to manage inner Observables, typically for handling asynchronous operations like API calls.
|
|
131
|
+
* `concatMap`: Maps to an inner Observable and processes them sequentially.
|
|
132
|
+
* `switchMap`: Maps to an inner Observable but cancels the previous inner subscription if a new outer value arrives.
|
|
133
|
+
* `mergeMap`: Maps to an inner Observable and processes them in parallel.
|
|
134
|
+
* `exhaustMap`: Maps to an inner Observable but ignores new outer values while the current inner Observable is still active.
|
|
135
|
+
|
|
136
|
+
* **Utility Operators:** Used for transformation, filtering, and side effects.
|
|
137
|
+
* `map`: Applies a given project function to each value emitted by the source Observable.
|
|
138
|
+
* `filter`: Emits only those values from the source Observable that pass a predicate function.
|
|
139
|
+
* `tap`: Perform a side effect for every emission on the source Observable, but return an Observable that is identical to the source.
|
|
140
|
+
* `delay`: Delays the emission of items from the source Observable by a given timeout.
|
|
141
|
+
* `catchError`: Catches errors on the source Observable and returns a new Observable or throws an error.
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
// Instead of: import { map, from } from 'rxjs';
|
|
145
|
+
// You can do:
|
|
146
|
+
import { createEffect, ofType } from 'rx-tiny-flux';
|
|
147
|
+
import { map, from } from 'rx-tiny-flux/rxjs';
|
|
148
|
+
|
|
149
|
+
// ... your effect implementation
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Selectors
|
|
155
|
+
|
|
156
|
+
Selectors are pure functions used for obtaining slices of store state. The library provides two factory functions for this:
|
|
157
|
+
|
|
158
|
+
1. `createFeatureSelector`: Selects a top-level slice of the state using a `jsonpath` expression.
|
|
159
|
+
2. `createSelector`: Composes multiple selectors to compute derived data.
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
import { createFeatureSelector, createSelector } from 'rx-tiny-flux';
|
|
163
|
+
|
|
164
|
+
// 1. We use `createFeatureSelector` with a jsonpath expression to get a top-level slice of the state.
|
|
165
|
+
const selectCounterSlice = createFeatureSelector('$.counter');
|
|
166
|
+
|
|
167
|
+
// 2. We use `createSelector` to compose and extract more granular data from the slice.
|
|
168
|
+
const selectCounterValue = createSelector(
|
|
169
|
+
selectCounterSlice,
|
|
170
|
+
(counter) => counter?.value // Added 'optional chaining' for safety
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const selectLastUpdate = createSelector(
|
|
174
|
+
selectCounterSlice,
|
|
175
|
+
(counter) => counter?.lastUpdate
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Using the selector with the store
|
|
179
|
+
store.select(selectCounterValue).subscribe((value) => {
|
|
180
|
+
console.log(`Counter value is now: ${value}`);
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Putting It All Together: The Store
|
|
187
|
+
|
|
188
|
+
The `Store` is the central piece that brings everything together. You instantiate it, register your reducers and effects, and then use it to dispatch actions and select state.
|
|
189
|
+
|
|
190
|
+
```javascript
|
|
191
|
+
// Library imports
|
|
192
|
+
import { Store } from 'rx-tiny-flux';
|
|
193
|
+
|
|
194
|
+
// Import all your application's reducers, effects, and actions
|
|
195
|
+
import { counterReducer, logReducer } from './path/to/your/reducers';
|
|
196
|
+
import { incrementAsyncEffect } from './path/to/your/effects';
|
|
197
|
+
import { increment, incrementAsync } from './path/to/your/actions';
|
|
198
|
+
|
|
199
|
+
// 1. The Store can start with an empty state.
|
|
200
|
+
const initialState = {};
|
|
201
|
+
|
|
202
|
+
// 2. Create a new Store instance
|
|
203
|
+
const store = new Store(initialState);
|
|
204
|
+
|
|
205
|
+
// 3. Register all reducers. The store will build its initial state from them.
|
|
206
|
+
store.registerReducers(counterReducer, logReducer);
|
|
207
|
+
|
|
208
|
+
// 4. Register all effects.
|
|
209
|
+
store.registerEffects(incrementAsyncEffect);
|
|
210
|
+
|
|
211
|
+
// 5. Dispatch actions to trigger state changes and side effects.
|
|
212
|
+
console.log('Dispatching actions...');
|
|
213
|
+
store.dispatch(increment());
|
|
214
|
+
store.dispatch(increment());
|
|
215
|
+
store.dispatch(incrementAsync()); // Will trigger the effect
|
|
216
|
+
|
|
217
|
+
// 6. Select and subscribe to state changes.
|
|
218
|
+
// It's crucial to capture the subscription object so we can unsubscribe later.
|
|
219
|
+
const counterSubscription = store.select(selectCounterValue).subscribe((value) => {
|
|
220
|
+
console.log(`Counter value is now: ${value}`);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// 7. Clean Up (Unsubscribe)
|
|
224
|
+
// In any real application, you must unsubscribe to prevent memory leaks.
|
|
225
|
+
// When the subscription is no longer needed (e.g., a component is destroyed),
|
|
226
|
+
// call the .unsubscribe() method.
|
|
227
|
+
counterSubscription.unsubscribe();
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### ZeppOS Integration (via ZML)
|
|
231
|
+
|
|
232
|
+
For developers using the `ZML` library on the ZeppOS platform, `rx-tiny-flux` offers an optional plugin that seamlessly integrates the store with the `BaseApp` and `BasePage` component lifecycle.
|
|
233
|
+
|
|
234
|
+
This plugin injects `dispatch` and `subscribe` methods into your component's instance. Most importantly, the `subscribe` method is lifecycle-aware: it automatically tracks all subscriptions and unsubscribes from them when the component's `onDestroy` hook is called, preventing common memory leaks.
|
|
235
|
+
|
|
236
|
+
#### How to Use
|
|
237
|
+
|
|
238
|
+
1. **Create and configure your store** as usual.
|
|
239
|
+
2. **Import the `storePlugin`** from `rx-tiny-flux/zeppos`.
|
|
240
|
+
3. **Register the plugin** globally for `BaseApp` and `BasePage` using the static `.use()` method, passing your store instance.
|
|
241
|
+
4. **Use `this.dispatch()` and `this.subscribe()`** inside your pages.
|
|
242
|
+
|
|
243
|
+
Here is a complete example:
|
|
244
|
+
|
|
245
|
+
```javascript
|
|
246
|
+
// app.js - Your application's entry point
|
|
247
|
+
import { BaseApp, BasePage } from '@zeppos/zml';
|
|
248
|
+
import { Store } from 'rx-tiny-flux';
|
|
249
|
+
import { storePlugin } from 'rx-tiny-flux/zeppos';
|
|
250
|
+
|
|
251
|
+
// 1. Import your reducers, actions, etc.
|
|
252
|
+
import { counterReducer } from './path/to/reducers';
|
|
253
|
+
import { selectCounterValue } from './path/to/selectors';
|
|
254
|
+
import { increment } from './path/to/actions';
|
|
255
|
+
|
|
256
|
+
// 2. Create your store instance
|
|
257
|
+
const store = new Store({});
|
|
258
|
+
store.registerReducers(counterReducer);
|
|
259
|
+
|
|
260
|
+
// 3. Register the plugin globally for App and Pages
|
|
261
|
+
BaseApp.use(storePlugin, store);
|
|
262
|
+
BasePage.use(storePlugin, store);
|
|
263
|
+
|
|
264
|
+
App(BaseApp({
|
|
265
|
+
// ... your App config
|
|
266
|
+
}));
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
```javascript
|
|
270
|
+
// page/index.js - An example page
|
|
271
|
+
import { BasePage, ui } from '@zeppos/zml';
|
|
272
|
+
import { selectCounterValue } from '../path/to/selectors';
|
|
273
|
+
import { increment } from './path/to/actions';
|
|
274
|
+
|
|
275
|
+
Page(BasePage({
|
|
276
|
+
build() {
|
|
277
|
+
const myText = ui.createWidget(ui.widget.TEXT, { /* ... */ });
|
|
278
|
+
|
|
279
|
+
// 4. Use `this.subscribe` to listen to state changes
|
|
280
|
+
this.subscribe(selectCounterValue, (value) => {
|
|
281
|
+
myText.setProperty(ui.prop.TEXT, `Counter: ${value}`);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Use `this.dispatch` to dispatch actions
|
|
285
|
+
ui.createWidget(ui.widget.BUTTON, {
|
|
286
|
+
// ...
|
|
287
|
+
click_func: () => this.dispatch(increment()),
|
|
288
|
+
});
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
onDestroy() {
|
|
292
|
+
// No need to unsubscribe manually!
|
|
293
|
+
// The storePlugin will do it for you automatically.
|
|
294
|
+
console.log('Page destroyed, subscriptions cleaned up.');
|
|
295
|
+
}
|
|
296
|
+
}));
|
|
297
|
+
```
|