Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Rolando Lopez and Ryan Jeffords
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Rolando Lopez@rolando_lopez ) and Ryan Jeffords@ryanjeffords )

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.



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

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

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.