Skip to main content
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Tony Peacock
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Tony Peacock

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

By
Published in

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.

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel