Managing state in Angular Apps with ngrx/platform

About me:

Software Engineer @ Trasys

stefanos-lignos.com

github

twitter

What is Application State?

A typical web application has various types of state

https://github.com/gothinkster/realworld
Typical types of state of a web app (Data State)
Data state
Data state
Typical types of state of a web app (Communication state)
Communication state
Communication state
Typical types of state of a web app (Control state, Session state, Location state)
Control state
Control state
Session state
Location state
Flux
  • MVC design pattern does not put any constraint when it comes to flow of direction of data between Model, View and Controller
  • Dispatcher is a single-point-of-emission for all actions in a flux application. The action is sent from the dispatcher to stores, which update themselves in response to the action
  • Stores manage the application state for a particular domain within the application.After the stores are updated, they broadcast an event declaring that their state has changed, so the views may query the new state and update themselves.
  • Views listen for changes from the stores and re-render themselves appropriately.
  • Actions are simple objects with a type property and some data.
MVC doesn’t scale
“Yeah, that was a tricky slide [the one with multiple models and views and bidirectional data flow], partly because there's not a lot of consensus for what MVC is exactly - lots of people have different ideas about what it is. What we're really arguing against is bi-directional data flow, where one change can loop back and have cascading effects.”
Redux
  • Like in Flux, in Redux data flow is unidirectional
  • Unlike Flux, there is a single store in Redux. Store is immutable. Every time a new object tree is created with data from the previous state plus what is changed
  • The change is performed by reducers which are pure functions with no side effects
  • No central Dispatcher in Redux. When an action needs to be executed then the dispatch() method of the store is called, passing the action as parameter. Then all listeners are informed the state has changed
Redux implementation
In 15 lines of code with RxJS
Redux-like implementation with RxJS

@Injectable()
export class Store extends BehaviorSubject {

	constructor(private dispatcher, private reducer, private initialState) {
		super(initialState);
		this.dispatcher.pipe(
			tap(val => console.log('ACTION: ', val)),
			scan((state, action) => this.reducer(state, action), initialState),
			tap(val => console.log('STATE: ', val)),
		).subscribe(state => super.next(state));
	}

	dispatch(action: any) {
		this.dispatcher.dispatch(action);
	}

	select(key: string) {
		return this.pipe(map(state => state[key]));
	}
}

export class Dispatcher extends Subject {
	dispatch(action: Action): void {
		this.next(action);
	}
}
					
RxJS: The Basics
RxJS is a library for reactive programming using Observables. In reactive programming the fundamental unit of work is the stream. Streams are nothing more than a sequence of events(mouse clicks, key presses, bits of network data) over time.

							Stream(loadMagazineEveryMonth$).pipe(
								filter(magazine => magazine.month === 'April')
							).subscribe(magazine => console.log(magazine.news));

							// What: I want to retrieve only the April edition.
							// How: I don't care..
						
RxJS: The Basics
  • RxJS is a JavaScript implementation of the Reactive Extensions. Rx is a reactive programming model originally created at Microsoft. Instead of streams in RxJS the main entity is the Observables
  • 
    								Observable {
    									producer => events
    									push values => consumer(observer)
    									unsubscribe();
    								}
    							
  • RxJS is mostly about Observables and Observers… but it’s also about Subjects.
  • 
    									Subject {
    										producer => events
    										push values => list of consumers(observers)
    										unsubscribe();
    										next(); complete(); error(); // can be both Observable and Observer
    									}
    							
  • While plain Observables are unicast (each subscribed Observer owns an independent execution of the Observable), Subjects are multicast. Values can be multicasted to many Observers.
  • Because of this and because of its twofold nature, Subject and BehaviourSubject are the best candidates for our store implementation.

Must read: Learning Observable By Building Observable / On The Subject Of Subjects (Ben Lesh)

Redux-like store with rxjs DEMO
Github repo
Demo Architecture
  • Angular uses Zone.js internally to trigger change detection on every browser event or http request or setTimeout or observable emission, etc.
  • The default change detection mechanism compares ('===') the current value with the previous value for each expression used in template for every component.
  • But, if a component depends only on its inputs, we can instruct angular to trigger change detection only if one of its inputs has changed (ChangeDetectionStrategy.OnPush).
  • This requires immutable input properties
ngrx/platform
ngrx/platform is a monorepo that consists of the following libs:
  • @ngrx/store - The core lib that gives us the tools to maintain state and dispatch actions.
  • @ngrx/schematics - Automates NgRx code generation. Scaffolding library that helps you generate files and code. Time-saver.
  • @ngrx/effects - Handles external interactions such as, network requests, web socket messages and time-based events. Isolates side effects from components.
  • @ngrx/router-store - Integrates NgRx with the Angular routing.
  • @ngrx/store-devtools - Tool for NgRx debugging (time travel debugging functionality).
  • @ngrx/entity - Provides an API to manipulate and query entity collections
OUR SECOND DEMO
Github repo
For this demo we used the nrwl/nx tool. Nrwl/nx is built on top of angular-cli and provides an opinionated approach to application project structure and patterns. Has adopted a monorepo approach
Project setup
  1. Install required libs
    npm install @ngrx/store --save
    npm install @ngrx/effects --save
    npm install @ngrx/router-store --save
    npm install @ngrx/store-devtools --save
    npm install ngrx-store-freeze --save
  2. Register NgRx with AppModule (app.module.ts)
    
    								imports: [
    									...
    									...
    									StoreModule.forRoot({ router: routerReducer }, 
    										{ metaReducers: !environment.production ? [storeFreeze] : [] }),
    									EffectsModule.forRoot([]),
    									!environment.production ? StoreDevtoolsModule.instrument() : [],
    									StoreRouterConnectingModule,
    								],
    								providers: [{ provide: RouterStateSerializer, useClass: CustomSerializer }],
    								
    							
Project setup (meta-reducers)
A meta-reducer is a higher order function that receives a reducer (function) as parameter and returns a new reducer(function).

							export function freeze(reducer: ActionReducer): ActionReducer {
								return function(state, action) {
									const nextState = reducer(state, action);
									Object.freeze(nextState);
									return nextState;
								}
							}
									
							@NgModule({
							imports: [
								StoreModule.forRoot(freeze(reducer))
							]
							})
							export class AppModule {}
		
							// running this in console we get 
							// 'Uncaught TypeError: Cannot assign to read only property 'test1''
							(function()
							{
								'use strict';
								console.log("FUN_1:");
								let test = {test1: 'aha'};
								test.test1 = 2; 
									
								Object.freeze(test); 
								test.test1 = 1;  
							}());
						
Project setup (router-store)
During each navigation cycle, a ROUTER_NAVIGATION || ROUTER_CANCEL || ROUTER_ERROR action is dispatched with a snapshot of the state in its payload, the SerializedRouterStateSnapshot.

							export interface SerializedRouterStateSnapshot {
								root: ActivatedRouteSnapshot;
								url: string;
							}
						
Project setup (router-store)
The ActivatedRouteSnapshot is a large complex structure. This can cause performance issues when used with the Store Devtools. In most cases, we need a piece of information from the ActivatedRouteSnapshot. Additionally, the router state snapshot is a mutable object, which can cause issues when developing with store freeze. We can override the default serializer with a custom serializer.

								export interface RouterStateUrl {
									url: string;
									params: Params;
									queryParams: Params;
								}
								  
								export interface State {
									router: RouterReducerState;
								}
								  
								export class CustomSerializer implements RouterStateSerializer {
									serialize(routerState: RouterStateSnapshot): RouterStateUrl {
										let route = routerState.root;
									
										while (route.firstChild) {
											route = route.firstChild;
										}
									
										const { url, root: { queryParams } } = routerState;
										const { params } = route;
									
										return { url, params, queryParams };
									}
								}	
						
store-devtools, time travel debugging
From debug prepsective, we care about two things
  1. Trace the actions were dispatched
  2. The impact of this actions to the store

We can conrtol time in our store by replaying dispatched actions.

Use case: Implement authentication with NgRx

Here are our goals:

  1. Show elements depending on a user being logged in or not
  2. Make routes unavailable if a user shouldn’t have access to them
  3. Handle unauthorized (Status 401) response
  4. Handle authentication lifecycle

Let's see some examples of these requirements in the demo

Authentication with NgRx
  1. Create the State interface (libs/src/+state/auth.interfaces.ts):
    
    								export interface Auth {
    									loggedIn: boolean;
    									user: User;
    									status: Status;
    								}
    								  
    								export interface AuthState {
    									readonly auth: Auth;
    								}
    								  
    								export interface User {
    									email: string;
    									token: string;
    									username: string;
    									bio: string;
    									image: string;
    								}
    
    								export type Status = 'INIT' | 'IN_PROGRESS';
    						
  2. Initialize the state (libs/src/+state/auth.init.ts):
    
    									export const authInitialState: Auth = {
    										loggedIn: false,
    										status: 'INIT',
    										user: {
    										  email: '',
    										  token: '',
    										  username: '',
    										  bio: '',
    										  image: ''
    										}
    									  };
    							
Fractal state management
  • In our app architecture we want to follow the seperation of concerns pronciple (reducers=>business logic, effects=>side effects, smart-dump components)
  • Things that have to do only with the authentication of our app should be isolated and decoupled. We have a module(lib) that encapsulates and abstracts all these related things
  • We have feature modules and each one of them contributes to the global state
  • NgRx provides us with a method called "forFeature()" that allows us to add the feature state to the global app state once the feature is loaded(eagerly, lazily).
Authentication with NgRx

3. Next we create the reducer and register it to the module (libs/src/+state/auth.reducer.ts).


						// (libs/src/+state/auth.reducer.ts)
						export function authReducer(state: Auth, action: AuthAction): Auth {
							switch (action.type) {
								default: {
									return state;
								}
							}
					

						// (libs/src/auth.module.ts)
						imports: [
							...
							StoreModule.forFeature('auth', authReducer, { initialState: authInitialState })
						]
					
Authentication with NgRx

Our world until now was synchronous.

Generally, NgRx promotes functional programming. Reducers, selectors, RxJS operators are pure functions. This means that they produce no side effects.

But, side effects are impossible to avoid. We can isolate side effects (http calls, read from a file, write to localStorage) through ngrx/effects so that the rest of our ngrx implementation be composed of pure functions.

    Effects:
  1. Listen for actions dispatched from @ngrx/store
  2. Isolate side effects from components, allowing for more pure components that select state and dispatch actions
  3. Provide a new action to reduce state based on external interactions such as network requests, web socket messages and time-based events.
Authentication with NgRx

4. Next we create the effects class and register it to the module (libs/src/+state/auth.effects.ts).


						// (libs/src/+state/auth.effects.ts)
						@Injectable()
						export class AuthEffects {
							@Effect()
							getUser ...
						}

					

							// (libs/src/auth.module.ts)
							imports: [
								...
								StoreModule.forFeature('auth', authReducer, { initialState: authInitialState }),
								EffectsModule.forFeature([AuthEffects])
							]
						
lazy modules with ngrx v2
Configure Login

							// (libs/auth/src/login/login.component.ts)
							submit() {
								this.store.dispatch(new fromActions.Login());
							}

							// (libs/auth/src/+state/auth.actions.ts)
							import { Action } from '@ngrx/store';
							export const enum AuthActionTypes {
								LOGIN = '[auth] LOGIN',
								LOGIN_SUCCESS = '[auth] LOGIN_SUCCESS',
								LOGIN_FAIL = '[auth] LOGIN_FAIL'
								...
							}
					
Configure Login

						// (libs/auth/src/+state/auth.effects.ts)
						@Effect()
						login = this.actions.ofType(AuthActionTypes.LOGIN).pipe(
							withLatestFrom(this.store.select(fromNgrxForms.getData)),
							exhaustMap(([action, data]) =>
								this.authService.authUser('LOGIN', data).pipe(
									map(result => new fromActions.LoginSuccess(result.user)),
									catchError(result => of(new fromNgrxForms.SetErrors(result.error.errors)))
								)
							)
						);
					
Configure Login

					// (libs/auth/src/+state/auth.reducers.ts)
					
					export function authReducer(state: Auth, action: AuthAction): Auth {
						switch (action.type) {case AuthActionTypes.LOGIN:
							...
							case AuthActionTypes.REGISTER: {
								return { ...state, status: 'IN_PROGRESS' };
							}
							case AuthActionTypes.REGISTER_SUCCESS:
							case AuthActionTypes.LOGIN_SUCCESS: {
								return {
									...state,
									loggedIn: true,
									status: 'INIT',
									user: action.payload
								};
							}
							case AuthActionTypes.LOGIN_FAIL:
							case AuthActionTypes.REGISTER_FAIL: {
								return { ...state, status: 'INIT' };
							}
							...
					
Configure Login

				@Effect({ dispatch: false })
				loginRegisterSuccess =
					this.actions.ofType(AuthActionTypes.LOGIN_SUCCESS, AuthActionTypes.REGISTER_SUCCESS).pipe(
						tap(action => {
							this.localStorageJwtService.setItem(action.payload.token);
							this.router.navigateByUrl('/');
						})
					);	
					
Show elements depending on a user being logged in or not

Selectors are just functions, that allow us, to get only the part of the application state, that we are interested in.Because selectors are pure functions, the last result can be returned when the arguments match without reinvoking your selector function. This can provide performance benefits.


						export const getAuth = createFeatureSelector('auth');
						export const getLoggedIn = createSelector(getAuth, (auth: Auth) => auth.loggedIn);
						export const getUser = createSelector(getAuth, (auth: Auth) => auth.user);
					
When to use NgRx

A mnemonic from Mike Ryan and Brandon Robert @ ng-conf

S hared state between components (Authentication)
H ydrated state from storage
A vailable state when re-entering routes
R etrieved state with a side effect
I mpacted state that is changed from other components or services

and also...

Optimistic updates
Undo/Redo logic
OnPush change detection strategy

Thank you!