rescope
Version:
Flexible state management system based on flux architecture, stores data components & inheritable scopes
296 lines (224 loc) • 8.46 kB
Markdown
# ReScope Stores
## Stability (the Store:apply function)
When state updates occurs, a store state stop being coherent with its results data.<br>
In ReScope stores, the role of the "apply" function is to make the store data predictable basing on its state.
This is done in 2 way :
- In a synchronized way, by returning a new data hashmap from the "apply" fn
```jsx
/*...*/
apply(data, new_state, changes_in_state){
let newData = {...data, ...state};
/* do synchrone interpolation & remaps in newData here */
return newData;
}
/*...*/
```
- In an async way :
- using this.push(newData) or by returning a referenced hashmap
- and mostly by using wait() & release() paired functions, which will keep the store unstable and lock its update propagation until all wait() calls becomes released.
```jsx
/*...*/
apply(data={}, new_state, changes_in_state){
let {async1={}, async2={}, async3={}} = data;
/* do async fill / update the data */
this.wait('anAsyncRabbit')
doSomeAsync(
(err,res)=>{
async1.datum=err||res;
this.release('anAsyncRabbit')
}
)
return {async1, async2, async3};
}
/*...*/
apply(data={}, new_state, changes_in_state){
this.wait('anAsyncRabbit')
doSomeAsync(
(err,res)=>{
this.push({async1:res, status:err||'ok'};// will replace data
this.release('anAsyncRabbit')
}
)
return data;
}
/*...*/
```
## A Store Workflow
- A Store have it's state updated ( action has pushed state update or a source store had its data updated )
- If this state have the required & followed value
- The apply function push new data in an async or sync way
- The store is stabilized and (if there is new data) propagated
- listening stores have theirs state updated and we go to step 1 until the whole scope is stable
## Stores initial state / restore
A store initialized with data will be stable synchronously when instantiated. <br>
If it only have a state but no data, the apply function will be called by the constructor synchronously. <br>
(implying that the Store object may not be fully initialized) <br>
## Actions & mutations
As the store stay independents, they deal with theirs own perimeters. <br>
The app state could be mutated using different methods depending the needs.
### Using actions
Actions could be dispatched from scopes or directly on the stores.
\* They are only called if theirs stores are active.
```jsx
class AppState extend Store{
static use = ["!AppConfig"];// require AppConfig to be applied & propagated
static actions = {
activateSalt(arg){// binded on the store instance
// return some state updates
return {some:'mutations'};
// or
return; // to not change the state
// wait, release, setState & push remain callable
// this.nextState contain the incoming state
}
}
}
```
### Using setState
All stores inherit the setState method. <br>
Once a store state is updated, the resulting data changes are automatically propagated to the followers.
### Using stores functions
The stores could be enhanced with functions & setters, that will ultimately update theirs state-data pairing.
```jsx
class AppState extend Store{
static use = ["!AppConfig"];// require AppConfig to be applied & propagated
static data = {};
switchTodoList(todoUrl){
this.setState({todoUrl})
// or
this.wait();
doSomeAsync(()=>{
this.data.stateChange = "stand"
this.release()
})
}
}
```
### push
Using push will update & propag the data of a store.
* This should be used with cautious as it could break the state-data coherence. (that said not all the stores needs to be predictable)
## Stores state & data serialization / restoration
Serialization & restoration is managed by the Scopes objects.<br>
Stores only have to maintain the state-data coherence, but can have initial state and data from different sources :<br>
- Using initial state & data :
```jsx
class MyStore extends Store {
static state = {};// initial state
static data = {};// initial data (soft cloned)
};
let MyStoreInstance = new MyStore(
BaseScope,
{
state : {}, // take static defined state/data precedence
data : {}
}
)
```
## How to add dependencies in a store
```jsx
export default class currentUser extends Store {
static use = ["appState", "session"];// here the source store that should be in the store scope
apply( data, { appState, session }, changes ) {
/*...*/
return data;
}
};
```
## How to "remap" dependencies value & sub-values in the state/data
```jsx
export default class myInterpolatedDataStore extends Store {
static use = {
"someSource.someValue" : "mySwitchValue",
"someSource2.someStuff.value" : "mySwitchValue2",
"someSource3.someValue" : "mySwitchValue3",
"someSource4.someValue" : "mySwitchValue4"
};
apply( data, { mySwitchValue, mySwitchValue2, mySwitchValue3, mySwitchValue4 }, changes ) {
/*...*/
return data;
}
};
```
## How to keep a store unstable until some stores / value is initialized
```jsx
export default class myInterpolatedDataStore extends Store {
static use = {
"!someSource.someValue" : "mySwitchValue",// require someSource.someValue != false
"!someRequieredSource" : true,
"someSource2" : "someSource2"
};
apply( data, { mySwitchValue, mySwitchValue2, mySwitchValue3, mySwitchValue4 }, changes ) {
/*...*/
return data;
}
};
```
## How to only call apply & update the store if specific changes occurs in the sources store
```jsx
export default class myInterpolatedDataStore extends Store {
static use = {
"!someSource.someValue" : "mySwitchValue",// require someSource.someValue != false
"!someRequieredSource" : true,
"someSource2" : "someSource2"
};
static follow = {// only call "apply" if one of these state keys has change
"someSource2":(newData)=>returnTrueIfApplicable(newdata),
"mySwitchValue":true, // just change
}
apply( data, { mySwitchValue, someSource2 }, changes ) {
/*...*/
return data;
}
};
```
## How to choose if data changes should be applied
```jsx
export default class myInterpolatedDataStore extends Store {
static use = {
"!someSource.someValue" : "mySwitchValue",// require someSource.someValue != false
"!someRequieredSource" : true,
"someSource2" : "someSource2"
};
hasDataChange( newDatas ) {
return super.hasDataChange(state);// default : compare old and new data & data.*
}
apply( data, { mySwitchValue, someSource2 }, changes ) {
/*...*/
return data;
}
};
```
## How to choose if data changes should be propagated
```jsx
export default class myInterpolatedDataStore extends Store {
static use = {
"!someSource.someValue" : "mySwitchValue",// require someSource.someValue != false
"!someRequieredSource" : true,
"someSource2" : "someSource2"
};
shouldPropag(data){
return true;
}
apply( data, { mySwitchValue, someSource2 }, changes ) {
/*...*/
return data;
}
};
```
## How to catch synchrone errors in the apply fn
```jsx
export default class testErrorCatch extends Rescope.Store {
static state = { ok: true };
static actions = {
makeError: v => ({ failNow: true })
};
apply( data, state ) {
if ( state.failNow )
throw new Error("oups")
return state;
}
handleError(error) {
this.push({ failNow: false, catched: true })
}
}
```