Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Shawn Grigson
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Shawn Grigson@shawngrig )

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

By Ben Nadel on

CAUTION: This post is a mostly a note-to-self, so that I can close the circle on a thought I was having yesterday.

Yesterday, I took a look at creating a simple, React-inspired .setState() store using an RxJS BehaviorSubject in Angular 6.1.10. Since a BehaviorSubject() is practically a state-store in and of itself, my simple store was nothing more than a thin wrapper around the BehaviorSubject() that exposed a .setState() method that proxied the underlying .next() method. In yesterday's demo, the .setState() method accepted a Partial<T> object that would be merged into a new state object; but, I wanted to update the method to also accept a callback in the same way that React's setState() method does.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

In yesterday's demo, the signature for the .setState() method looked like this:

  • public setState( partialState: Partial<StateType> ) : void;

It accepted a partial collection of state properties which it then merged into the existing state, creating a new state object. This works great, if you know the new values ahead of time. But, sometimes, you need to define the new state in the context of the current state. In such cases, I'd like to expose a .setState() signature that accepts a callback that exposes the current state:

  • // When updating the state, the caller has the option to define the new state partial
  • // using a a callback. This callback will provide the current state snapshot.
  • interface SetStateCallback<T> {
  • ( currentState: T ): Partial<T>;
  • }
  •  
  • // ....
  •  
  • public setState( callback: SetStateCallback<StateType> ) : void;

Using TypeScript's method overloading, we can combine these two signatures and provide a flexible .setState() method for the calling context:

  • // 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 );
  •  
  • }

As you can see, the .setState() method now accepts either a partial object or a function callback. Internally, we then check to see which type of updater we have and then use the updater to generate the partial payload that we subsequently merge into the new state object.

NOTE: I am putting "_" in front of the overload method parameters since they can't be referenced in the method body - it's just a mental note.

All together, the SimpleStore<T> generic class now looks like this:

  • // Import the core angular services.
  • import { BehaviorSubject } from "rxjs";
  • import { distinctUntilChanged } from "rxjs/operators";
  • import { Observable } from "rxjs";
  • import { map } from "rxjs/operators";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • // When updating the state, the caller has the option to define the new state partial
  • // using a a callback. This callback will provide the current state snapshot.
  • interface SetStateCallback<T> {
  • ( currentState: T ): Partial<T>;
  • }
  •  
  • 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.asObservable() );
  •  
  • }
  •  
  •  
  • // 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 (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 );
  •  
  • }
  •  
  • }

To test this, I put together a very simple counter demo in Angular 6.1.10 that increments and decrements a property of the store using a .setState() callback:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  • import { Observable } from "rxjs";
  •  
  • // Import the application components and services.
  • import { SimpleStore } from "./simple.store";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • interface CounterStore {
  • counter: number;
  • };
  •  
  • @Component({
  • selector: "my-app",
  • styleUrls: [ "./app.component.less" ],
  • template:
  • `
  • <p class="counter">
  • <strong>Counter:</strong> {{ counter | async }}
  • </p>
  •  
  • <p class="buttons">
  • <button (click)="increment( 1 )"> Increment </button>
  • <button (click)="increment( -1 )"> Decrement </button>
  • </p>
  • `
  • })
  • export class AppComponent {
  •  
  • public counter: Observable<number>;
  •  
  • private counterStore: SimpleStore<CounterStore>;
  •  
  • // I initialize the app component.
  • constructor() {
  •  
  • this.counterStore = new SimpleStore({
  • counter: 0
  • });
  • this.counter = this.counterStore.select( "counter" );
  •  
  • // Use normal "partial" to update the simple store.
  • this.counterStore.setState({
  • counter: 10
  • });
  •  
  • }
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  • // I increment the counter value in the simple store.
  • public increment( delta: number ) : void {
  •  
  • // Use updater "function" to update the simple store.
  • this.counterStore.setState(
  • ( state ) => {
  •  
  • return({
  • counter: ( state.counter + delta )
  • });
  •  
  • }
  • );
  •  
  • }
  •  
  • }

As you can see, when we increment the counter, our .setState() callback uses the current counter value to calculate the next counter state. And, when we run this in the browser and hit "Increment" a few times, we get the following output:


 
 
 

 
 Providing a callback to the .setState() function of a React-inspired simple store in Angular 6.1.10. 
 
 
 

As you can see, the .setState() callback works quite nicely.

Anyway, I just wanted to close the circle on that .setState() experiment in Angular 6.1.10. I had remembered the callback-based signature the moment after I posted yesterday's code.



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

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.