Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Andy Weber and Gunnar Lieb and Thilo Hermann
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Andy Weber , Gunnar Lieb@akitogo ) , and Thilo Hermann ( @thfusion_de )

Creating A Simple setState() Store Using An RxJS BehaviorSubject In Angular 6.1.10

By Ben Nadel on

After struggling to wrap my head around Redux-inspired state management, I wanted to step back and consider simpler solutions. I'm not saying that a simpler solution is the right solution - only that moving my brain in a different direction may help me make new connections. As such, I wanted to see what it would look like to create a React-inspired state management class that exposes a .setState() method. I briefly noodled on this concept 3-years ago with AngularJS; but, with an RxJS BehaviorSubject() at my finger tips, I suspect that implementing such a state management class will be dead simple.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

A couple of days ago, I went down a rabbit-hole with RxJS streams before realizing that I was searching for the "wrong type of simplicity". In the end - after I hit rock bottom - I ended up choosing an RxJS BehaviorSubject() because it gave me everything that I needed with an easy-to-understand abstraction. Having done that, it occurred to me that the BehaviorSubject() is practically a state management class in and of itself. So, I wanted to see what it would look like to wrap the RxJS BehaviorSubject() class in a thin proxy class that exposes a .setState() method:

  • // Import the core angular services.
  • import { BehaviorSubject } from "rxjs";
  • import { distinctUntilChanged } from "rxjs/operators";
  • import { Observable } from "rxjs";
  • import { map } from "rxjs/operators";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • export class SimpleStore<StateType = any> {
  •  
  • private stateSubject: BehaviorSubject<StateType>;
  •  
  • // I initialize the simple store with the givne initial state value.
  • constructor( initialState: StateType ) {
  •  
  • this.stateSubject = new BehaviorSubject( initialState );
  •  
  • }
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  • // I get the current state as a stream (will always emit the current state value as
  • // the first item in the stream).
  • public getState(): Observable<StateType> {
  •  
  • return( this.stateSubject.pipe( distinctUntilChanged() ) );
  •  
  • }
  •  
  •  
  • // I get the current state snapshot.
  • public getStateSnapshot() : StateType {
  •  
  • return( this.stateSubject.getValue() );
  •  
  • }
  •  
  •  
  • // I return the given top-level state key as a stream (will always emit the current
  • // key value as the first item in the stream).
  • public select<K extends keyof StateType>( key: K ) : Observable<StateType[K]> {
  •  
  • var selectStream = this.stateSubject.pipe(
  • map(
  • ( state: StateType ) => {
  •  
  • return( state[ key ] );
  •  
  • }
  • ),
  • distinctUntilChanged()
  • );
  •  
  • return( selectStream );
  •  
  • }
  •  
  •  
  • // I move the store to a new state by merging the given partial state into the
  • // existing state (creating a new state object).
  • // --
  • // CAUTION: Partial<T> does not currently project against "undefined" values. This is
  • // a known type safety issue in TypeScript.
  • public setState( partialState: Partial<StateType> ) : void {
  •  
  • var currentState = this.getStateSnapshot();
  • var nextState = Object.assign( {}, currentState, partialState );
  •  
  • this.stateSubject.next( nextState );
  •  
  • }
  •  
  • }

As you can see, this class does little more than delegate calls to the underlying BehaviorSubject(). In fact, the .setState() method is simply assembling the next value to emit on the BehaviorSubject() stream.

Now, let's look at how this can be consumed. I've put together a simple Angular 6.1.10 demo that creates a SimpleStore class for baby names. Then, subscribes to the names before trying to change them:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { SimpleStore } from "./simple.store";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • interface NameStore {
  • girl: string;
  • boy: string;
  • };
  •  
  • @Component({
  • selector: "my-app",
  • styleUrls: [ "./app.component.less" ],
  • template:
  • `
  • <em>See console output</em>.
  • `
  • })
  • export class AppComponent {
  •  
  • // I initialize the app component.
  • constructor() {
  •  
  • // Create a simple store for baby name selection.
  • var babyNames = new SimpleStore<NameStore>({
  • girl: "Jill",
  • boy: "John"
  • });
  •  
  • // --
  •  
  • // Subscribe to any changes in the state (a new state object is created every
  • // time setState() is called).
  • babyNames.getState().subscribe(
  • ( state ) => {
  • console.log( "New state..." );
  • }
  • );
  •  
  • // Subscribe to the individual name selections. Since these are unique changes,
  • // these callbacks will be called far less often than the getState() stream.
  • babyNames.select( "girl" ).subscribe(
  • ( name ) => {
  • console.log( "Girl's Name:", name );
  • }
  • );
  • babyNames.select( "boy" ).subscribe(
  • ( name ) => {
  • console.log( "Boy's Name:", name );
  • }
  • );
  •  
  • // --
  •  
  • // Try changing up some state!
  • babyNames.setState({
  • girl: "Kim"
  • });
  • babyNames.setState({
  • girl: "Kim" // Duplicate.
  • });
  • babyNames.setState({
  • girl: "Kim", // Duplicate.
  • boy: "Tim"
  • });
  • babyNames.setState({
  • girl: "Kim" // Duplicate.
  • });
  • babyNames.setState({
  • girl: "Joanna"
  • });
  • babyNames.setState({
  • girl: "Joanna" // Duplicate.
  • });
  • babyNames.setState({
  • girl: "Joanna" // Duplicate.
  • });
  •  
  • }
  •  
  • }

In this code, we're subscribing to the top-level state as well as the individual names. A new top-level state object gets created every time we call .setState(); so, we expect the top-level observer to be called a lot. But, when we subscribe to the individual state keys (ie, names), our streams end with a distinctUntilChanged() operator. As such, these observer callbacks should only be invoked when the actual values change.

And, if we run this Angular application in the browser, we get the following output:


 
 
 

 
 Simple store with RxJS BehaviorSubject allows key-specific subscriptions. 
 
 
 

As you can see, our getState() subscription callback is invoked every time the .setState() method is called. However, since our name-based streams are "distinct until changed", the key-level subscription callbacks are only invoked when the actual name values change.

I don't know how a simple state store like this fits into a wider state management picture. But, I'm having trouble creating a larger mental model for state management. As such, I think it's helpful to drop-down and solve smaller problems such that I may have the building blocks needed for a larger mental model. If nothing else, this builds a greater appreciation for RxJS BehaviorSubject() streams, which have proven to be very useful.



Reader Comments

@All,

Having just posted this, it just occurred to me that the .setState() method in React can actually accept a callback so that you can get access to the existing state while trying to calculate the next state. It would look something like this:

.setState(
	( currentState ) => {
		return({
			value: ( currentState.value + 1 )
		});
	}
)
Reply to this Comment

D'oh -- hit the submit too quickly. What I meant to say was that it would be relatively easy to update the .setState() method to allow for either a Partial<T> or a function that would return the partial.

I'll see if I can put that together quickly later.

Reply to this Comment

@All,

I just wanted to quickly jump in with an update that allows the .setState() method to accept either a Partial<T> or a callback. It looks something like this:

// I move the store to a new state by merging the given (or generated) partial state
// into the existing state (creating a new state object).
// --
// CAUTION: Partial<T> does not currently project against "undefined" values. This is
// a known type safety issue in TypeScript.
public setState( _callback: SetStateCallback<StateType> ) : void;
public setState( _partialState: Partial<StateType> ) : void;
public setState( updater: any ) : void {

	var currentState = this.getStateSnapshot();
	// If the updater is a function, then it will need the current state in order to
	// generate the next state. Otherwise, the updater is the Partial<T> object.
	// --
	// NOTE: There's no need for try/catch here since the updater() function will
	// fail before the internal state is updated (if it has a bug in it). As such, it
	// will naturally push the error-handling to the calling context, which makes
	// sense for this type of workflow.
	var partialState = ( updater instanceof Function )
		? updater( currentState )
		: updater
	;
	var nextState = Object.assign( {}, currentState, partialState );

	this.stateSubject.next( nextState );

}

Read more here: https://www.bennadel.com/blog/3523-creating-a-simple-setstate-store-using-an-rxjs-behaviorsubject-in-angular-6-1-10---part-2.htm

Reply to this Comment

@Netanel,

A couple of people have recommended Akita to me so far. I will definitely take a look. There was some complexity in it that felt weird to me, but I don't remember what it was. Maybe something about models or entities or something. But, I'm still trying to wrap my head around all of this - I am sure once I sort out some things in my head, I'll be able to return to Akita and make more sense of it.

Sometimes, I have to bang my head against the wall a bunch before I understand what other people are doing :D

Reply to this Comment

Hey, your implemantaion is great!

Just a quick question ? how can I 'select' an INNER field of an object.

lets say that I have a state

export class GlobalState {

data: IGlobal  = INIT_DATA;

constructor(data?) {
  this.data = data ? data : [];
}

}

now when I getState().select('HERE I GET ONLY data') ...
Is it possible to allow the select to be deep ?

Reply to this Comment

@NSN,

" now when I getState().select('HERE I GET ONLY data') "

actually is .select('....') without getState() ofcourse.

Reply to this Comment

@Ben,

Hey, since its not possible to edit, I just want to clear myself

I look for something like

.select('data.someInnerFieldOfData').subscribe()

is this even possible ?

Reply to this Comment

@NSN,

Good question. I actually tried to implement something like that in my first approach. I don't think it was possible with a single string (that contains dots); but, I think I may have gotten something to work if you pass in an individual argument for each traversal operation. Something like:

.select( "data", "someInnerField", "oneMoreJump" ).subscribe()

Then, internally, I had something like:

select( ...keys: string[] ) {
	// .... reduce stream based on key segments.
}

I am not sure if I got it working or not. I think maybe I got something working; but, it looked really janky internally. And, frankly, I'm not that good with RxJS. If you want to see how crazy the TypeScript can get, take a look at the NgRx "selector" class:

https://github.com/ngrx/platform/blob/master/modules/store/src/selector.ts

.... pretty crazy!

Ultimately, I abandoned it because I am not a fan of having really deeply nested state. My applications have several stores -- not just a single Redux-style Store that contains "all the things". As such, I don't really get much friction from having to poke at store objects. Usually just one or two keys at most. But, that's just my personal approach.

Reply to this Comment

@Ben,

"Ultimately, I abandoned it because I am not a fan of having really deeply nested state. My applications have several stores -"

So i guess I work just fine.

Its just that sometimes the state becomes big with alots of fields..

How do you keep it small ?

Reply to this Comment

@NSN,

To be honest, I don't have a lot of experience with Store. I am developing my own personal style as I dig into all of this stuff. As you can see, this post itself is only a few months old.

That said, I try to keep my Stores oriented around a "feature" of the application, not the application itself. So, I might have an "authentication" store or a "user list" store. Then, the amount of data in each store is implicitly limited by the scope of the feature.

I also wrap my Store inside something I call a "Runtime", which is really just the encapsulation of a Store for a particular feature. So, going back to the "authentication" store, really I would have an AuthenticationRuntime that wraps Store. Then, it would be up to the AuthenticationRuntime to expose RxJS streams for underlying properties.

I talk more thoroughly about this "Runtime" abstraction in a post from a few weeks ago:

https://www.bennadel.com/blog/3584-some-real-world-experimenting-with-the-runtime-abstraction-for-state-management-in-angular-7-2-7.htm

But, going back to your "select" question, the Runtime provides me with a translation layer to get at those inner keys. So, for example, if the Runtime needed to expose a stream for data.someInnerValue, it might have a method like:

public getInnerValue() : Observable<InnerValueType> {

	return this.store.select( "data" ).map(
		( data ) => {
			
			return( data.innerValue );
		
		}
	);

}

So, essentially that path traversal in moved from the "Store" into the "Runtime". This way, the calling context (ie, your components) still don't have to worry about the complexity and they just consume the targeted value without having to worry how it's stored in the underlying state.

Sorry, that's probably a lot to take in -- and, it likely different than a lot of the state management stuff you've seen out in the wild. I'm trying to find patterns that my brain can make sense of without have to have a masters degree in RxJS :D I see too many stream operators and my brain melts. So, I am trying to find ways to leverage Streams without making them too much of a hurdle.

Reply to this Comment

@NSN,

Oh, and I forgot that .map() has been replaced with .pipe( map() ). Just writing stuff off-the-cuff here.

Reply to this Comment

@Ben,

Thanks for your answers!

another one ? :)
what about sharing the subscription correctly ?

Reply to this Comment

@NSN,

So, sharing a subscription, from my understanding of RxJS, has more to with "Hot" vs "Cold" streams. In this case, The SimpleStore doesn't own the source of the data - it just manages the internal BehaviorSubject. As such, there's no sense that it can prevent data from being moved around until someone Subscribes to a stream.

In other words, calls to .setState() are going to happen regardless of whether or not anyone is subscribed to the state stream. So, essentially, the SmipleStore is a hot stream of data that's flowing regardless of subscription state. This means that you don't have to worry about sharing subscriptions because people will just get whatever data comes through after they subscribe.

Now, because the underlying structure is a BehaviorSubject, it does remember the last pushed value. So, to that end, every subscription will get the last pushed value, even if that value was pushed prior to the subscription.

Hope that makes sense. My mental model for RxJS is not super strong - in fact, I think the RxJS community is moving away from the terms "Hot" and "Cold" re: streams. So, take what I am saying with some caution :D

Reply to this Comment

@NSN,

Assuming you would want the parent data to be immutable, then I would just expose a method that overwrites data with a new innerValue property. Something like:

public setInnerValue( newInnerValue ) : void {

	var state = this.store.getStateSnapshot();
	
	this.store.setState({
		data: {
			// Start by creating a new object with the old data properties
			// being spread into the key-space.
			...state.data,
			
			// Then, overwrite the old "innerValue" property with the new
			// property being provided.
			innerValue: newInnerValue
		}
	});

}

It's a bit tedious, but not overly complex. Essentially, you are creating a shallow-copy of data and then appending the new innerValue property. This is the same general approach that frameworks like Redux use.

The shallower you can keep your Store, the easier this kind of stuff becomes.

Reply to this Comment

@Ben

I managed to do something like that

setInnerValue<K extends keyof StateType, S extends keyof StateType[K], T extends keyof StateType[K][S]>(newVal:any, mainKey: K, secKey: S, thirdKey?: T) {

if(thirdKey) {
  let curr = this.state[mainKey][secKey];

  curr[thirdKey] = newVal;
  let nextState = Object.assign( {}, this.state[mainKey][secKey], curr );
  this.updateState(nextState);

} else {
  
  let curr = this.state[mainKey];

  curr[secKey] = newVal;
  let nextState = Object.assign( {}, this.state[mainKey], curr );
  this.updateState(nextState);
}

}

but this is good only for 2/3 inner fields, How can I make this more generic ?

Reply to this Comment
Sorry for writing so much but this is very interesting..

I managed to selectInner and setInner values, but my problem is that when I setInner only the specific stream is being triggered, and not the parent


public selectInner<K extends keyof StateType, S extends keyof StateType[K], T extends keyof StateType[K][S]>( mainKey: K, secKey: S, thirdKey?: T ) 
    : Observable<any> {
    
    var selectStream = this.state$.pipe(
        map(
            ( state: StateType ) => {
              
              if(thirdKey) {
                return state[mainKey][secKey][thirdKey];
              } else {
                return state[mainKey][secKey];
              }
              
            }
        ),
        distinctUntilChanged()
    );

    return( selectStream );

  }


and set


  setInnerValue<K extends keyof StateType, S extends keyof StateType[K], T extends keyof StateType[K][S]>(newVal:any, mainKey: K, secKey: S, thirdKey?: T) {
    
    if(thirdKey) {
      // console.log("charArea is changing!! here");
      
      let curr = this.state[mainKey][secKey];
  
      curr[thirdKey] = newVal;
      let nextState = Object.assign( {}, this.state[mainKey][secKey], curr );
      this.updateState(nextState);

    } else {
      // console.log("charArea is changing!! there");
      let curr = this.state[mainKey];
  
      curr[secKey] = newVal;
      let nextState = Object.assign( {}, this.state[mainKey], curr );
      this.updateState(nextState);
    }

    
  }

in component

this.crecorderService.crs.select('chartArea').subscribe(x => console.log("charArea Observer",x));
this.crecorderService.crs.selectInner('chartArea','isMoving').subscribe(x => console.log("charArea field is changing!!!",x));


this.crecorderService.crs.setInnerValue(true,'chartArea','isMoving');
timer(4000).subscribe(x => this.crecorderService.crs.setInnerValue(false,'chartArea','isMoving'));


and output in console is:
charArea Observer chartArea {selectedFileName: "", zoomEnabled: true, isMultipleAxis: false, isMoving: false, isRecording: false, …}
recorder.component.ts:168 charArea field is changing!!! false
recorder.component.ts:168 charArea field is changing!!! true
recorder.component.ts:168 charArea field is changing!!! false


 what do you think ? Thanks very much m8.
Reply to this Comment

@NSN,

This is getting a bit over my head :D RxJS is still something I am struggling with. Even some of the TypeScript here is getting over my head (all the extends kind of things are hard for my brain to process). As far as why the stream isn't updating, it's not obvious to me. It looks like you are creating new objects, so it should see that the reference changed.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.