DEV Community

loading...
Cover image for S-FLUX V3, The power of Flux pattern with the ease of redux

S-FLUX V3, The power of Flux pattern with the ease of redux

nomoredeps profile image NoMoreDeps ・6 min read

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.

Alt Text

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.

What you do in V3 :
Alt Text

What it does internally :
Alt Text

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
});
Enter fullscreen mode Exit fullscreen mode

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 ;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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]
  });
}
Enter fullscreen mode Exit fullscreen mode

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&lt;CounterState>, payload: { value: number }) {
  this.nextState(payload);
}
Enter fullscreen mode Exit fullscreen mode

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 ;
    }
    ...
}
Enter fullscreen mode Exit fullscreen mode

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"
};
Enter fullscreen mode Exit fullscreen mode

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 : ""
});
Enter fullscreen mode Exit fullscreen mode

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 };
  }
Enter fullscreen mode Exit fullscreen mode

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();
}, []);
Enter fullscreen mode Exit fullscreen mode

Using the store helper

The usage of a helper is extremely simple. Just import the helper.

import BlogStore from "./stores/Blog";
Enter fullscreen mode Exit fullscreen mode

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();
})
Enter fullscreen mode Exit fullscreen mode
// 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();
})
Enter fullscreen mode Exit fullscreen mode

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
        });
      }/>
  </>;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

I hope you liked this post.

If you want to know more, here are all the links

Website

Npm

Github

Enjoy πŸ˜„

Discussion (0)

pic
Editor guide