Skip to main content
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Steven Guitar
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Steven Guitar

Experimenting With The Runtime Abstraction For State Management In Angular 7.0.3

By
Published in Comments (12)

CAUTION: This post is really just me thinking out loud on state management concepts in Angular 7. Consider this nothing but a work in progress from the mind of someone who barely understands what they are doing.

Earlier this week, I shared my current mental model for state management and the separation of concerns in an Angular application. After banging my head against Redux, I realized that what I wanted was an abstraction that completely hid Redux from me; an abstraction that would allow me to choose Redux later on, should I actually need it. I called this abstraction the "Runtime". This is very similar to the "Workflow" abstraction that I discussed 2-years ago (and very similar to the Facade and Sandbox abstractions discussed in my previous posts). Today, I wanted to start experimenting with some concrete code to help exercise the abstraction and see if it is something that I actually want to use. As a test harness, I created a little app for "Santa's Christmas List" that helps track "nice" and "naughty" citizens.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

Because the goal of the Runtime abstraction was to hide the very notion of state management from the View, let's start with the App Component. The App component presents a list of Nice and Naughty people as well as a form that will allow the user to add a new person to the currently-selected list.

As an initial pass on this experiment, all of the values coming out of the Runtime abstraction are RxJS Observable streams. These streams emits values when the internal state of the Runtime changes. And, the internal state of the Runtime can be mutated using various public methods.

// Import the core angular services.
import { Component } from "@angular/core";
import { Observable } from "rxjs";

// Import the application components and services.
import { ListType } from "./santa.runtime";
import { Person } from "./santa.runtime";
import { SantaRuntime } from "./santa.runtime";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Component({
	selector: "my-app",
	styleUrls: [ "./app.component.less" ],
	templateUrl: "./app.component.htm"
})
export class AppComponent {

	public intake: {
		name: string;
	};
	public selectedListType: Observable<ListType>;
	public people: Observable<Person[]>;
	public niceCount: Observable<number>;
	public naughtyCount: Observable<number>;

	private santaRuntime: SantaRuntime;

	// I initialize the app component.
	constructor( santaRuntime: SantaRuntime ) {

		this.santaRuntime = santaRuntime;

		// The intake form only operates on local state. There's no need for this to be
		// a concern of the runtime (though, if this demo were more complicated, it is
		// possible that error-messages could be controlled by the runtime).
		this.intake = {
			name: ""
		};

		// Hook up the various runtime streams.
		this.selectedListType = this.santaRuntime.getSelectedListType();
		this.people = this.santaRuntime.getPeople();
		this.niceCount = this.santaRuntime.getNiceCount();
		this.naughtyCount = this.santaRuntime.getNaughtyCount();

	}

	// ---
	// PUBLIC METHODS.
	// ---

	// I get called once after the inputs have been bound for the first time.
	public ngOnInit() : void {

		var hash = window.location.hash.slice( 1 ).toLowerCase();

		// If the window location (a VIEW CONCERN) is indicating a list selection, then
		// let's update the runtime to match the list selection.
		if ( ( hash === "nice" ) || ( hash === "naughty" ) ) {

			this.santaRuntime.selectList( hash );

		}

	}


	// I process the new person intake form for Santa's list.
	public processIntake() : void {

		if ( ! this.intake.name ) {

			return;

		}

		// NOTE: We don't have to pass in a list-type because the currently selected list
		// is already part of the runtime internal state. As such, we only have to pass
		// in the name of the person and the runtime will take care of the rest.
		this.santaRuntime.addPerson( this.intake.name );
		this.intake.name = "";

	}


	// I remove the given person from Santa's lists.
	public removePerson( person: any ) : void {

		this.santaRuntime.removePerson( person.id );

	}


	// I show the given list of people.
	public showList( list: ListType ) : void {

		// Update the location hash (a VIEW CONCERN) so that we start on the selected
		// list if the browser is refreshed.
		window.location.hash = list;

		this.santaRuntime.selectList( list );

	}

}

As you can see, the App Component injects the SantaRuntime and then immediately binds to various state-streams, like SelectedListType and People. You can also see that it declares a local object - instead of a stream binding - for the person intake form. There's no need for this Form information to be maintained in the Runtime state - it is a view concern (in this application).

The nice thing about the Runtime abstraction is that it keeps the App component super simple. There's almost no logic here other than the logic that tries to sync the Browser URL and the selected list type.

The downside is that the component property names feel super janky to me. They read as static values; but, in actuality, they are streams. Really, I should be appending a suffix like "Stream" (as in "PeopleStream") to these property names. But, I figured that, for a first-pass, I would try to keep things as simple as possible.

ASIDE: A lot of people seem to append the "$" suffix to indicate an RxJS "stream". I am not sure where this came from or why that was chosen. As such, I don't want to copy this approach blindly. It also smacks of "Hungarian Notation", which is something we struggled with back in the jQuery days.

Naming aside, I also feel like the use of streams here makes my App Component view syntax look rather janky:

<h2>
	Santa's Christmas List
</h2>

<nav class="nav">
	<a
		(click)="showList( 'nice' )"
		class="nav__item"
		[class.nav__item--selected]="( ( selectedListType | async ) === 'nice' )">
		Nice ({{ niceCount | async }})
	</a>
	<a
		(click)="showList( 'naughty' )"
		class="nav__item"
		[class.nav__item--selected]="( ( selectedListType | async ) === 'naughty' )">
		Naughty ({{ naughtyCount | async }})
	</a>
</nav>

<div class="list-view">

	<form (submit)="processIntake()" class="list-view__form intake">
		<input
			type="text"
			name="name"
			[(ngModel)]="intake.name"
			placeholder="Name..."
			class="intake__name"
		/>
		<button type="submit" class="intake__submit">
			Add Person
		</button>
	</form>

	<ul *ngIf="( people | async )?.length" class="list-view__list list">
		<li *ngFor="let person of ( people | async )" class="list__item person">

			<span class="person__name">
				{{ person.name }}
			</span>

			<a (click)="removePerson( person )" class="person__delete">
				Delete
			</a>

		</li>
	</ul>

</div>

As you can see, the component template is littered with Async Pipe operators. And, several of my variables need to be async'd more than once. I know that the benefit of this approach is that I don't need to manage the RxJS subscriptions in the component logic; but, at what cost? Perhaps this is just something that people get used to.

Before we dig into the Runtime, let's take a look at the rendered application now that we see how the App component is going to consume the Runtime. If we load this in the browser and add some people to Santa's list, we get the following browser output:

Experimenting with the Runtime abstraction in Angular 7.0.3 to encapsulate choices around state management.

Naming and syntax choices aside, the main goal of this experiment was to abstract-away state management; which, the Runtime has done. So, let's take a look at the SantaRuntime to see what it is doing. Internally, it encapsulates a simple storage object which is little more than a thin proxy to the RxJS Behavior Subject. The bulk of the Runtime is just coordinating access and mutation to this underlying state object.

// Import the core angular services.
import { combineLatest } from "rxjs";
import { Injectable } from "@angular/core";
import { map } from "rxjs/operators";
import { Observable } from "rxjs";

// Import the application components and services.
import { AppStorageService } from "./app-storage.service";
import { SimpleStore } from "./simple.store";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

export interface SantaState {
	v: number;
	selectedListType: ListType;
	nicePeople: Person[];
	naughtyPeople: Person[];
}

export interface Person {
	id: number;
	name: string;
}

export type ListType = "nice" | "naughty";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Injectable({
	providedIn: "root"
})
export class SantaRuntime {

	private appStorage: AppStorageService;
	private appStorageKey: string;
	private store: SimpleStore<SantaState>;

	// I initialize the Santa runtime.
	constructor( appStorage: AppStorageService ) {

		this.appStorage = appStorage;
		this.appStorageKey = "santa_runtime_storage";

		// NOTE: For the store instance we are NOT USING DEPENDENCY-INJECTION. That's
		// because the store isn't really a "behavior" that we would ever want to swap -
		// it's just a slightly more complex data structure. In reality, it's just a
		// fancy hash/object that can also emit values.
		this.store = new SimpleStore( this.getInitialState() );

		// Because this runtime wants to persist data across page reloads, let's register
		// an unload callback so that we have a chance to save the data as the app is
		// being unloaded.
		this.appStorage.registerUnloadCallback( this.saveToStorage );

	}

	// ---
	// PUBLIC METHODS.
	// ---

	// I add the given person to the currently-selected list.
	public async addPerson( name: string ) : Promise<number> {

		var person = {
			id: Date.now(),
			name: name
		};

		var state = this.store.getSnapshot();

		if ( state.selectedListType === "nice" ) {

			this.store.setState({
				nicePeople: state.nicePeople.concat( person )
			});

		} else {

			this.store.setState({
				naughtyPeople: state.naughtyPeople.concat( person )
			});

		}

		return( person.id );

	}


	// I return a stream that contains the number of people on the naughty list.
	public getNaughtyCount() : Observable<number> {

		return( this.getListCount( "naughtyPeople" ) );

	}


	// I return a stream that contains the number of people on the nice list.
	public getNiceCount() : Observable<number> {

		return( this.getListCount( "nicePeople" ) );

	}


	// I return a stream that contains the people in the currently-selected list.
	public getPeople() : Observable<Person[]> {

		var stream = combineLatest(
			this.store.select( "selectedListType" ),
			this.store.select( "nicePeople" ),
			this.store.select( "naughtyPeople" )
		);

		var reducedStream = stream.pipe(
			map(
				([ selectedListType, nicePeople, naughtyPeople ]) => {

					if ( selectedListType === "nice" ) {

						return( nicePeople );

					} else {

						return( naughtyPeople );

					}

				}
			)
		);

		return( reducedStream );

	}


	// I return a stream that contains the currently selected list type.
	public getSelectedListType() : Observable<ListType> {

		return( this.store.select( "selectedListType" ) );

	}


	// I remove the person with given ID from the naughty and nice lists.
	public async removePerson( id: number ) : Promise<void> {

		var state = this.store.getSnapshot();
		var nicePeople = state.nicePeople;
		var naughtyPeople = state.naughtyPeople;

		// Keep the people that don't have a matching ID.
		var filterInWithoutId = ( person: Person ) : boolean => {

			return( person.id !== id );

		};

		this.store.setState({
			nicePeople: nicePeople.filter( filterInWithoutId ),
			naughtyPeople: naughtyPeople.filter( filterInWithoutId )
		});

	}


	// I select the given list.
	public async selectList( listType: ListType ) : Promise<void> {

		this.store.setState({
			selectedListType: listType
		});

	}

	// ---
	// PRIVATE METHODS.
	// ---

	// I return the initial state for the underlying store.
	private getInitialState() : SantaState {

		// NOTE: Because we are using a string-literal as a "type", we have to help
		// TypeScript by using a type annotation on our initial state. Otherwise, it
		// won't be able to infer that our string is compatible with the type.
		var initialState: SantaState = {
			v: 3,
			selectedListType: "nice",
			nicePeople: [
				{
					id: 1,
					name: "Jon"
				}
			],
			naughtyPeople: [
				{
					id: 2,
					name: "Seema"
				}
			]
		};

		// See if we have any persisted store (returns NULL if not available).
		// --
		// CAUTION: The parent function is being called in a way that is expecting the
		// execution to by SYNCHRONOUS, which localStorage is. If the AppStorageService
		// were to return a Promise<data>, it would be less "blocking"; but, it would
		// also require us to rework the way we are initialing the store.
		var savedState = this.appStorage.loadData<SantaState>( this.appStorageKey );

		// If we have saved data AND the data structure is the same VERSION as the one
		// we expect, then return it as the initial state.
		if ( savedState && ( savedState.v === initialState.v ) ) {

			return( savedState );

		} else {

			return( initialState );

		}

	}


	// I return a stream that contains the count for the given Person collection.
	private getListCount( list: "nicePeople" | "naughtyPeople" ) : Observable<number> {

		var stream = this.store.select( list );

		var reducedStream = stream.pipe(
			map(
				( people ) => {

					return( people.length );

				}
			)
		);

		return( reducedStream );

	}


	// I save the current state to given store object.
	// --
	// CAUTION: Using a fat-arrow function to bind callback to instance.
	private saveToStorage = () : void => {

		this.appStorage.saveData( this.appStorageKey, this.store.getSnapshot() );

	}

}

As you can see, the mutation methods on the Runtime are just proxies to the underlying store. This is where you could substitute-in something like Redux or NgRX; but, in this case, my simple store feels completely sufficient.

This is also where you could manage asynchronous workflows like API requests and calls to other external data stores. Even though my workflows are entirely synchronous, you'll notice that all of my mutation methods return a Promise so that the calling context sees them as asyncrhonous. This leaves wiggle room in the future to refactor the internal workflow without having to change the calling context logic. The use of a Promise also allows a "Success" and "Failure" indication to be returned to the calling context, which will make error handling easier (as opposed to having to manage error handling completely within the state).

One thing that doesn't sit well with me is that there is no access to the underlying state outside of the exposed RxJS streams. It seems unfortunate to lock a component into a single consumption methodology. Really, what I'd like is for the Runtime to expose both snapshot and stream-based data so that the consuming components can use whatever approach they feel most comfortable with. This is definitely something that I'll have to noodle on more in my next experiment.

The other thing worth noting in this Runtime is that it injects the AppStorageService. This AppStorageService allows for data to be persisted and then loaded across multiple application life-cycles. Unlike a single state tree, this Runtime-based state management forces each Runtime to explicitly decide if and how it will persist data. And, in this case, I am using the AppStorageService to persist data to the LocalStorage API.

Notice that right after I inject the AppStorageService, I am registering an "unload" callback. By hooking into the "unload" event of the window, I am not constantly synchronizing data with the synchronous LocalStorage API. Instead, I'm waiting for the app to unload before I block and persist my data.

And, here are the details of the AppStorageService - it's basically a thin wrapper around the LocalStorage API and the window "unload" event:

// Import the core angular services.
import { Injectable } from "@angular/core";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

interface UnloadCallback {
	( service?: AppStorageService ) : void;
}


@Injectable({
	providedIn: "root"
})
export class AppStorageService {

	private unloadCallbacks: UnloadCallback[];

	// I initialize the app storage service.
	constructor() {

		this.unloadCallbacks = [];

		// The app storage service will be the central point of unload for the
		// application. As such, we can register a single handler here and then simply
		// consume the collection of unload callbacks.
		window.addEventListener( "beforeunload", this.handleUnload, false );

	}

	// ---
	// PUBLIC METHODS.
	// ---

	// I load the data with the given key from the persistent store. Returns null if
	// the storage device or the data is unavailable.
	public loadData<T = any>( key: string ) : T | null {

		try {

			var value = window.localStorage.getItem( key );

			if ( value !== null ) {

				window.localStorage.removeItem( key );

				return( JSON.parse( value ) );

			}

		} catch ( error ) {

			// Swallow error for now....

		}

		return( null );

	}


	// I register the given unload callback.
	public registerUnloadCallback( callback: UnloadCallback ) : void {

		this.unloadCallbacks.push( callback );

	}


	// I save the given data to the persistent storage using the given key.
	public saveData( key: string, data: any ) : void {

		try {

			window.localStorage.setItem( key, JSON.stringify( data ) );

		} catch ( error ) {

			// Swallow error for now....

		}

	}

	// ---
	// PRIVATE METHODS.
	// ---

	// I handle the unload event for the application.
	private handleUnload = ( event: any ) : void => {

		for ( var callback of this.unloadCallbacks ) {

			try {

				callback( this );

			} catch ( error ) {

				console.group( "App Unload Callback Error" );
				console.log( callback );
				console.error( error );
				console.groupEnd();

			}

		}

	}

}

And, just for completeness, here's the current version of the SimpleStore class that I am using (which was mostly discussed in my previous post):

// 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> | undefined;
}

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 snapshot.
	public getSnapshot() : StateType {

		return( this.stateSubject.getValue() );

	}


	// 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 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.getSnapshot();
		// 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
		;

		// If the updater function returned undefined, then it decided that no state
		// needed to be changed. In that case, just return-out.
		if ( partialState === undefined ) {

			return;

		}

		var nextState = Object.assign( {}, currentState, partialState );

		this.stateSubject.next( nextState );

	}

}

Overall, I do like the Runtime abstraction. I like that it gives me a place to put complex business logic and asynchronous control flow. I also like that it lives longer than the relevant View components, which provides an opportunity to synchronize data even when it is not being rendered.

What I don't like so far is the Stream-only API. I think in my next experiment, I'll have to build in some sort of way to access snapshot data so that I have the option to not litter my templates with Async Pipe operators.

This experiment wasn't complex to the point where I needed a Message Bus to communicate inter-Runtime messages. After all, there was only one Runtime. I think once I have a local API and methodology that feels comfortable, adding a Message Bus won't be all that challenging.

Anyway, this was just me "thinking out loud" on state management. If you have any feedback or constructive criticism, I'd love to hear it.

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

Reader Comments

449 Comments

Ben. This is amazing stuff although I got a bit lost in the SimpleStore code.

Can you explain what the 'K' stands for in:

K extends keyof StateType

Can I use any character or does it have to be 'K'?

I get a bit freaked out when looking at TypeScript Generics;)

15,902 Comments

@Charles,

No problem -- I had a post earlier that just looked at the SimpleStore class:

www.bennadel.com/blog/3522-creating-a-simple-setstate-store-using-an-rxjs-behaviorsubject-in-angular-6-1-10.htm

... which is basically just a thin wrapper around the RxJS BehaviorSubject.

Glad you're liking this kind of exploration. I'm really trying hard to wrap my head around this stuff. Much of the state management stuff I see feels overly complicated for my brain. So, I'm trying to see if I can come up with more straightforward. For me, I would rather type a little bit more if it means that I can more clearly see what is going on (as opposed to leaning on all kinds of functional composition and meta-programming).

For the Generics stuff, I'm still learning most of this myself. As far as I understand it, K extends keyof StateType just means that the set of valid values of K is defined by the key-set of StateType. In other words, K has to be a property of StateType. So, in terms of the select() method, it's saying that the value I pass to select() has to be a key of the underlying state. That way, TypeScript can make guarantees about the type-safety.

Honestly, that's the most advanced Generics concept I have ever used.

449 Comments

Ben. Thanks for the comprehensive reply.

I have been looking at the official TypeScript docs on Generics, so what you are saying now makes a lot more sense! The official docs are really well written. Each section builds up in complexity. Some of the stuff at the bottom is insane, but interesting to read!

3 Comments

Hello ben,

i really appreciate your code , and i am looking for exactly same because of rxjs heavy to me. coming to the point, your code is working in [ng build ] but if run , [ng build --prod] then i am getting error, see below.

ERROR in : Can't resolve all parameters for SimpleStore in store.ts: (?).

the constructor is failing , if i comment the constructor then code works, can you help me. why cant you use generic T instead of StateType = any

3 Comments

@Chinna,
i have these packages, do you think i have to upgrade?
{
"@angular-mdc/theme": "^0.41.1",
"@angular-mdc/web": "^0.41.1",
"@angular/animations": "~7.0.0",
"@angular/cdk": "^7.0.3",
"@angular/common": "~7.0.0",
"@angular/compiler": "~7.0.0",
"@angular/core": "~7.0.0",
"@angular/fire": "^5.1.0",
"@angular/flex-layout": "^7.0.0-beta.19",
"@angular/forms": "~7.0.0",
"@angular/http": "~7.0.0",
"@angular/material": "^7.0.3",
"@angular/platform-browser": "~7.0.0",
"@angular/platform-browser-dynamic": "~7.0.0",
"@angular/router": "~7.0.0",
"@ngx-loading-bar/http-client": "^2.2.0",
"@ngx-loading-bar/router": "^2.2.0",
"@sentry/browser": "^4.2.4",
"core-js": "^2.5.4",
"dexie": "^2.0.4",
"firebase": "^5.5.7",
"firebaseui": "^3.4.1",
"firebaseui-angular": "^3.3.2",
"hammerjs": "^2.0.8",
"ngx-logger": "^3.3.6",
"ngx-toastr": "^9.1.1",
"rxjs": "~6.3.3",
"stacktrace-js": "^2.0.0",
"web-animations-js": "^2.3.1",
"zone.js": "~0.8.26"
}

15,902 Comments

@Chinna,

That's a strange error. It looks like it can't can resolve dependency-injection arguments for SimpleStore. However, this class -- at least in my example -- is not using dependency-injection - it's being instantiated directly within the Runtime abstraction. As such, I am not sure what that error is meaning.

As far as using a Generic of T vs StateType = any, I think it's roughly the same thing. I'm just naming it StateType instead of T; and, I'm providing an explicit callback to any.

15,902 Comments

@All,

One thing that I really didn't like about this approach was the jankiness of the AsyncPipe syntax in the template. But, taking inspiration from something Jason Awbrey said on Twitter, I wanted to revisit this approach using Smart and Presentation components:

www.bennadel.com/blog/3528-using-presentation-components-in-order-to-hide-async-pipe-complexity-in-angular-7-0-3.htm

This approach quarantines the async pipe syntax inside the thin glue layer of the "smart" component.

3 Comments

@Ben,

Thank you very much sir, there is no problem your code and i going to use in prod. excellent job with clean generic code.

15,902 Comments

@Chinna,

This morning, I started to try and refactor my Incident Commander application to use this Runtime abstraction. First step is going to be slipping out the Smart / Presentation component, which is proving a little challenging. It's a funky step going from assuming you have everything right there to having to take inputs and emit outputs. Hopefully should have something to share later this week.

What will make it kind of interesting, I think, is that my IC tool uses Firebase in the background, which is a streaming API. So, as you type, the changes are synchronized across browsers. It will be fun to see how this works with the runtime abstraction (which I am hoping will hide all of that complexity).

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