Saturday, November 23, 2019

Store state management with RXJS BehaviorSubject

Basically, we will be creating and using an observer with the help of RXJS library. If you would like to see more of the RXJS in action you can take a look at this Angular course.


It is important to notice that the store will keep the immutable state principle. Which means that all the operations we are about to do on the stored objects inside will not modify the store itself, but will instead produce and return a new store.
Using immutability brings benefits such as preventing data race conditions as well as working with a different from the latest state of the same object when several observers are interacting with it.

For the demonstration, we will create 2 HTML buttons, which when triggered will increment or decrement the values of #inc element data
<button id="inc">+</button> <button id="dec">-</button>

<span id="state"></span>

#state will respond to changes inside of our observed data.

Then, to use behavioursubject we will import it from the rxjs library. It has the ability to save the last emitted value inside itself (thus imitating state) and when a subscriber asks for the saved data (subscribes to the behavior subject) the behavior subject will emit it.

import { BehaviorSubject } from "rxjs";

// we create a class Store with initial state object which has data inside( name and votes). All this initialization is done inside the constructor of the class.
class Store {
  constructor() {
    this.initialState = {
      data: [
        {
          name: "initial name",
          votes: 0
        }
      ]
    };

// we create a new behavior subject and load up the initialState data inside.
    this.subject$ = new BehaviorSubject(this.initialState);

// we create an observable from the subject, in order to be able to access the data from the behavior subject as read-only (i.e not to be able to write and populate values to other subscribers, thus creating chaos inside the data logic).
    this.state$ = this.subject$.asObservable();
  }

// We then use two functions (getters and setters, which get and set the state inside the behavior subject. (with .next() we emit values to the subject, .getValue() is not used very often, and here is just to get the current value inside the subject)
)

  get state() {
    return this.subject$.getValue();
  }
  setState(nextState) { // the function can be also defined as: set state()
    this.subject$.next(nextState);
  }
}

// now it is time to create an object from our previously defined class Store();
const store = new Store();

// When we click on the + button we set a new state inside of our store.

// Note that we have data and state, so each state is characterized with its own data. Here we just modify the initial(old) state with our data object {name, votes}.
document.querySelector("#inc").onclick = function() {
  store.setState(
    //add new objects
    {
      ...store.state,
      data: [...store.state.data, { name: "initial name", votes: 0 }]
    }
  );
};

Searching/querying objects inside the store. In our store, we can also have objects with different names. Here is how to do it: we start by attaching an event handler to the click on the + button:

document.querySelector("#inc").onclick = function() {
// this time we can search for a particular named object, we are interested in inside the store
let searched_name = "initial name";
  store.setState(
    {
      ...store.state,
      data : store.state.data.map(store_data => { // we loop through the whole store
        // console.log('data inside the store: '+JSON.stringify(inner_data));
        if (store_data.name === searched_name) {

// and if we have a match we change the corresponding store object by incrementing its current votes property with 1

          return {...store_data, votes: store_data.votes + 1 };
        }
        return store_data; // after the whole mapping logic, we return the modified 'stored_data' variable to be a property of the 'data' variable.
      })
    }
  );

Note: inside setState we return not a modified old store.set object, but a newly created object so keeping the immutability rule true.

// Here is how to delete objects based on searched_name variable. We loop throughout all the store data, compare and return only the objects which are not equal to the deleted value. Once again keep an eye on the immutability principle, that we are returning a completely new object and don't modify the store.state object by deleting its entries.
document.querySelector("#dec").onclick = function() {

 store.setState(
{
      ...store.state,
      data : store.state.data.filter(store_data => {
    return    store_data.name !== searched_name
      })
    }
  );
};
});

Last but not least if we want our HTML to reflect on the changes from the store we subscribe to BehaviorSubject:
store.state$.subscribe(state => {
// we can either display the current state to the console
  console.log("current state: " + JSON.stringify(state));
// or just place it inside our #state span element
document.querySelector('#state').innerHTML = state.votes;
});

...

Here is an alternative version, which achieves a similar functionality:

//initially we set out empty state object {}:
let initialState = {};

class AppState {
// we again create our behavior subject to store the empty initial state
  private stateSubject = new BehaviorSubject(initialState);
//then we create an observable out of the subject
  state$ = this.stateSubject.asObservable().pipe(
    scan((acc, newVal) => { // on every new value that the subject receives, via the scan function, we get it, together with its previous value acc (scan() in rxjs() behaves like reduce in javascript)
      return { ...acc, ...newVal }; // we create a new object consisting of the old accumulator value plus the newValue changes applied
    })
  );

// here is the important dispatch function, which simply via .next() places a new payload into a specific object key, thus ensuring that the particular object will have its new state set.
  dispatch(obj) {
    this.stateSubject.next(
      { [obj.key]: obj.payload }
    );
  }

}

// optionally we can debug the state inside the observable using Angular's async and json pipes:
{{ appState.state$ | async | json }}

// Here is how we can use the dispatch method: we just send new data to a specific key of our state.
this.appState.dispatch({
      key: 'person',
      payload: {
        name: '',
        website: ''
      }
    });

Congratulations and enjoy the Angular course!

Subscribe To My Channel for updates