S-Flux is a unidirectional dataflow implementation, which allows synchronizing the actions of an application to ensure visual consistency of data, and keep simplicity in their management, even if the amount of information grows over time.
The FLux pattern is a representation of a unidirectional data flow. This way of representing the life cycle of an information will ensure that for each cycle, all components using the data will be at the same level of information.
The Dispatcher will stack the Actions, then redistribute them to the Stores, which in turn will process or not the information. Once the information has been processed, each Store will notify the View(s) that more up-to-date data is available. The View(s) will then update their display accordingly.
An Action can be triggered by a View, a Store, or any other source, such as a server. In all cases, each Action will be processed sequentially by the Dispatcher.
S-Flux V3 provides a very streamlined approach, so you don't need to declare either the dispatcher
or the stores
. Only a definition of store actions is necessary, and Shadow-Flux generates on-the-fly all the helper methods, as well as all the subscription methods, with full typescript intellisense.
Sample definition
// State type
type CounterState = {
counter: number;
};
// Events
const _events = Flux.withEvents({
ForceCounter: ""
})
type TThis = TBaseStore<CounterState>;
// Actions
const _actions = {
async increase(this: TThis) {
this.nextState({
counter: this.getState().counter + 1
});
},
async decrease(this: TThis) {
this.nextState({
counter: this.getState().counter - 1
});
},
async setCounter(this: TThis, payload: { counter: number }) {
this.nextState({
counter: payload.counter
});
return _events.ForceCounter; // Return specific event
}
}
export default Flux.registerStore({
id: "Counter",
async init(): CounterState {
return { counter: 0 };
},
actions : _actions,
events : _events
});
Sample result
import CounterStore from "path-to-store";
// CounterStore will be of type :
{
getState() : CounterState ;
id : string ;
events: {
All : string ;
ForceCounter : string ;
},
subscribeTo: {
All : ((state: CounterState) => void) => { off: () => void };
ForceCounter : ((state: CounterState) => void) => { off: () => void };
},
actions: {
increase : () => void ;
decrease : () => void ;
setCounter : (payload: { counter: number }) => void ;
}
}
The CounterStore object will provide all actions methods generated from the store actions, and all subscription method for each events.
Overview
In the original Flux pattern, you need to follow these step :
- Instanciate a dispatcher
- Create one or more store(s)
- Register all stores with the dispatcher
- Instanciate a subscriber helper
- Creates Action-Helper functions to manage Action creations
- Subscribe Store events
But now, with S-Flux v3, to get the same result you only need to :
- Declare one or more store(s)
- Use the store helper to access all available actions
- Use the store helper to subscribe all available events
Stores
In S-Flux, stores are used to store information. A store is responsible for a precise perimeter, called scope. This perimeter will include its data model, as well as the business or technical rules it needs.
Basically, a store is a class that extends the BaseStore class. This class must describe the data model, as well as the rules applicable to this data model. To do this, the data model will be stored in a State field, and the rules described by methods that can be triggered externally by the application.
A store should only manage its own data. However, it can rely on data from another store, if necessary, during the execution of one of its rules, to fill its data.
In order to simplify the declaration, as well as store usage, S-Flux has introduced a simplification on top of the original Pattern, allowing to hide a certain complexity level, and reducing at the same time some tedious declarations necessary to retreive data. Be aware that the simplification is a helper, but underneath, the original pattern is kept and used by S-Flux.
Actions
Actions are methods that aim to modify the state of stored data. An action will receive a payload providing the necessary information to modify the state accordingly.
The method prototype can be describe as follow :
type TActionReturn = void | null | string | string[];
async actionName(payload?: any) => TActionReturn;
async actionName(payload?: any, For?: (...ids: string[]) => Promise<void>) => TActionReturn;
The payload is an object containing all the information useful for the action to update its State.
The For
method can be used to ensure any other store will finish to process the payload before the current one will continue.Ex:
async getTravelDetail(payload: TTravel, For: TAwaitFor) {
await For(priceStore.id);
this.nextState({
...payload,De
price: priceStore.getState().prices[payload.travelId]
});
}
The nexstate
method is used to update the current state. By default, the state is replaced by the new value. If you want to partially update the state, you can set the flag mergeToPreviousState to true. Be aware that the update is a shallow merge, not a deep merge.
// Since we declare the action outside the Store class,
// we can set the this parameter to set the correct store context
async setCounterValue(this: TBaseStore<CounterState>, payload: { value: number }) {
this.nextState(payload);
}
The return value will determine the event(s) that will be triggered at the end of processing. By default, for any change of state, an event of type "All" will be triggered. It is possible to trigger specific events in case of a partial change of state. The different combinations are described below.
Value | Effect |
---|---|
undefined |
Global change, we notify all subscribers to any change |
null |
No change, this will prevent any event to be triggered |
string |
The selected event will be triggered |
string[] |
The selected events will be triggered |
The wrapper returned by the "registerStore" method will create as many Helper methods as you have defined an action. But in addition to that, the methods also take care of forward the Payload directly to the dispatcher.
// So if you have defined this
const _actions = {
async increase(this: TThis) {
...
},
async decrease(this: TThis) {
...
},
async setCounter(this: TThis, payload: { counter: number }) {
...
}
}
// You get that in return
{
...
actions: {
increase : () => void ;
decrease : () => void ;
setCounter : (payload: { counter: number }) => void ;
}
...
}
Events
Events are used to notify subscribers that a change in the state of a store they subscribe to has been updated. By default, it is not necessary to create events, because a global event exists, named "All". It is propagated for any change in a store, unless otherwise specified.
For each event to which you wish to subscribe, you must register through the Dispatcher, specifying the Store ID and the name of the event. The wrapper returned by the "registerStore" method will create as many Helper methods as you have defined an event. These methods will allow a direct subscription, without the constraint of passing the store identifier and the name of the event, and this, without even using the Dispatcher.
Events are described as key/value pairs, carrying the name of the event.
const events = {
updated : "updated",
newEntry : "newEntry"
};
As it is a bit redundant to specify for each key its value, knowing that in most cases the value will probably be identical to the key, it is possible to use the withEvents function to simplify this syntax.
const events = withEvents({
updated : "",
newEntry : ""
});
Each wrapped method will be generated followign each event declared, plus the All events that will be added automatically.
subscribeTo: {
All : ((state: TheStoreState) => void) => { off: () => void };
updated : ((state: TheStoreState) => void) => { off: () => void };
newEntry : ((state: TheStoreState) => void) => { off: () => void };
}
The returned value of a subscription is an object exposing an off method. You can call it to unsubscribe at any time.
An example with React Hook
React.useEffect(() => {
const sub = counterStore.subscribeTo.All(_ => setState(_));
return () => sub.off();
}, []);
Using the store helper
The usage of a helper is extremely simple. Just import the helper.
import BlogStore from "./stores/Blog";
Let's assume that the Store owns the following actions :
- getLatestArticles
- getArticleDetail
- postComment
And provide the following events:
- OnGetList
- OnGetDetail
- OnPostComment
The Helper
will automatically provide an action
method group and a subscription
method group.
To call the actions, use the methods in actions, and for events, the one in subscribeTo
.
// You can subscribe globally to any change, and update your full state
React.useEffect(() => {
const sub = BlogStore.subscribeTo.All(state => setState(state));
// Unsubscribe when unmount
return () => sub.off();
})
// You can subscribe specifically to one partial event
React.useEffect(() => {
const sub = BlogStore.subscribeTo.OnGetList(state => setLatest(state.latest));
// Unsubscribe when unmount
return () => sub.off();
})
Now to call an action, just use the action group methods
// You can call easily any action
function Detail() {
return <>
<BlogDetail
body = {state.selectedBlog}
onComment = {
_ => BlogStore.actions.postComment({
id : _.id,
content : _.content
});
}/>
</>;
}
Conclusion
I hope you liked this post.
If you want to know more, here are all the links
Enjoy 😄
Top comments (0)