rstore
v0.4.5
Published
a reactive state manager
Downloads
67
Maintainers
Readme
rstore
a straightforward, explicit, declarative and composable reactive store/model
Introduction
RStore is an observable (reactive) model.
Reactive?
| Active | Reactive | |---|---| | direct query at some point in time | a subscription to all updates | | console.log(state.getValue()) | state.subscribe(value => console.log(value)) |
In the reactive world of observables you don't have to think if something has happened by the time something else happens. So you can concentrate on a single thing - defining a way the changes combine and map to the result.
What are the benefits?
- a state is defined in one place
store(0)
- every observer immediately receives a notification on a state update (including the initial state)
store(0)
.subscribe(value => console.log(value))
- the state can only be updated by other observables, so it changes whenever source stream produce updates
- the way updates modify the state is defined in terms of (widely adopted redux) reducers (see the interoperability section)
store(0)
.plug(Rx.Observable.of(1), (x, y) => x + y)
.subscribe(value => console.log(value))
- observables (sources of updates) can be defined in many ways
- RxJS 5
- Bacon.js
- Most.js
- built-in factories:
- fromEvent(element: HTMLElement, EventName: String) => Observable of events
- interval(n: number, ...values) => Observable that emits values every n milliseconds
- address() => Observable that acts like an Event Bus (see counters example)
- custom observer that implements observable interface
- RStore stores can be combined
const s1 = store(1);
const s2 = store(2);
s1.plug(s2, (x, y) => x + y)
.subscribe(value => console.log(value)); // 3
- RStore stores can be converted to RxJS Observables
store(1)
.toRx()
.map(x => x* 2)
.subscribe(state => console.log(state));
Example:
1 ) describe a model (properties, initial state):
const {store} = require('rstore');
const myStore = store(0);
2 ) define sources of changes (using fromEvent
helper or Rx, Bacon, Most.js streams, see defining inputs)
const inc$ = fromEvent(document.getElementById('inc'), 'click');
const dec$ = fromEvent(document.getElementById('dec'), 'click');
3 ) define how these changes affect the model:
myStore
.plug(inc$, (state, _update) => state + 1)
.plug(dec$, (state, _update) => state - 1);
4 ) subscribe to the store and get an updated model on every change:
myStore.subscribe(model => console.log(model));
API
Creating stores
As RStore supports multiple kinds of observables (APIs), it exports 5 store constructors:
| constructor | description | |---|---| | store | general, with an autodetect of the observable API | | storeR | RStore observables | | storeBacon | Bacon.js | | storeMost | Most.js | | storeRx | RxJS5 |
A constructor is a function that expects a single argument - an initial model value: It could be a primitive value
store(0);
or an object
store({
todos: []
});
RStore methods
| method | description |
|---|---|
| .plug(oObservable, fnReducer)
| subscribes RStore to the oObservable
and for every value calls the fnReducer
|
| .unplug(oObservable, fnReducer)
| given oObservable
and optionally fnReducer
unsubscribes RStore from an external observer |
| .subscribe(fnObserver)
| subscribes to RStore - calls fnObserver
for every state update, including an initial value |
| .reset()
| unsubscribes everything|
| .toRx(Rx)
| converts a store to an Rx Observable |
Interoperability
###RxJS5 Observables are sources of changes
store({
c1 : 0,
c2 : 0
}).plug(
Rx.Observable.of(1), (s, u) => Object.assign({}, s, {c1: u}),
Rx.Observable.of(2), (s, u) => Object.assign({}, s, {c2: u})
).subscribe(model => console.log(model.c1 + model.c2));
A store can be converted to an observable
store(2)
.toRx(Rx) // Rx object, optional if Rx is global
.map(x => x * 2)
.subscribe(v => console.log(v)); // 4
###Redux Reducers can be reused
const observableOfChanges = Rx.Observable.from([
{type: 'INCREMENT'},
{type: 'INCREMENT'},
{type: 'DECREMENT'},
{type: 'INCREMENT'},
]);
const reduxReducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state;
}
};
store(0)
.plug(observableOfChanges, reduxReducer)
.subscribe(state => console.log(state)); // 2
Redux store as a source of updates
const changes = [
{type: 'INCREMENT'},
{type: 'INCREMENT'},
{type: 'DECREMENT'},
{type: 'INCREMENT'}
];
function reduxReducer (state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
const reduxStore = Redux.createStore(reduxReducer);
changes.forEach(m => reduxStore.dispatch(m));
rstore.store(0)
.plug(reduxStore[Symbol.observable](), (s, u) => u)
.subscribe(state => console.log(state)); // 4
changes.forEach(m => reduxStore.dispatch(m));
Composability
As stores are Observables, they can be used as sources of changes:
// store 1 (counter from the previous example)
const counter1 = store(0);
const inc1$ = fromEvent(document.getElementById('inc1'), 'click');
const dec1$ = fromEvent(document.getElementById('dec1'), 'click');
counter1
.plug(inc1$, (state, _update) => state + 1)
.plug(dec1$, (state, _update) => state - 1);
counter1.subscribe(model => document.getElementById('res1').textContent = model);
// store 2 (a second counter, similar to counter 1)
const counter2 = store(0);
const inc2$ = fromEvent(document.getElementById('inc2'), 'click');
const dec2$ = fromEvent(document.getElementById('dec2'), 'click');
counter2
.plug(inc2$, (state, _update) => state + 1)
.plug(dec2$, (state, _update) => state - 1);
counter2.subscribe(model => document.getElementById('res2').textContent = model);
// a combining store, stores current states from both counters
// notifies subscribers if any of the counter values changes
store({
c1 : 0,
c2 : 0
}).plug(
counter1, (s, u) => Object.assign({}, s, {c1: u}),
counter2, (s, u) => Object.assign({}, s, {c2: u})
).subscribe(model => document.getElementById('total').textContent = model.c1 + model.c2);
Redux DevTools Extension
STK works with Redux Devtools Extension:
- Install the tools
- Connect one of the stores to the DevTools:
const initial = 0;
const store = rstore.store(initial);
rstore.devtools.addStore(store, initial);
easy access to model fields
In many cases model modification functions are just setters that take the current state and a new value as inputs and produce a new model. It might be hard to access\update deeply nested fields as it is desirable that functions stay pure and data stays immutable.
rstore has a lens
function that is a pair of a setter and a getter for the given field:
const lens = require('rstore').lens;
const l = lens('a');
const o1 = {a: 11};
const o2 = {a: 22};
// returns a value of the field 'a' in the object 'o1' (11)
console.log( l.get(o1) )
// returns a value of the field 'a' in the object 'o2' (22)
console.log( l.get(o2) )
// returns a new object like 'o1', but the field 'a' is set to '33'
// ({a: 33}), object 'o1' stays the same
console.log( l.set(o1, 33) )
// returns a new object like 'o2', but the field 'a' is set to '44'
// ({a: 44}), object 'o2' stays the same
console.log( l.set(o2, 44) )