@ngrx store - rohit120582sharma/Documentation GitHub Wiki

Introduction

ngrx is a collection of reactive libraries for angular, containing a redux implementation and many other useful libraries.

Redux is a great pattern for centralizing state management with immutable data structures. This concept is called Single Source of Truth.

Redux is all about simplyfying how we store and update application state. It also ensures a one-directional flow, by making the state read-only.

There are 3 major components which help to set-up this system:

  • Store
    • The store is where all of application state lives, acting as the database for the web application. The state is immutable, but it can be altered by explicitly defined actions.
  • Reducer
    • If the store is the database of the application, the reducers are the tables.
    • A reducer is a pure function that will accept a state and an action as parameters, and based on the action, will give you a new representation of the state. That means, it takes the state, makes a copy of it and then applies the changes to the copy.
  • Action
    • An action decribes what to change in the store. The 'action' is only mechanism the app has to communicate with the store.
    • Actions represent payloads of information that are dispatched to the store from the application and are usually triggered by user interaction. An action is composed a type and a payload.

Reference


Overview

The store encompasses the whole state. When an action is dispatched, the reducer takes it along with the previous state, applies the payload depending on the action type, and outputs the new state. The reducers return fragments of the state, and actions are pre-defined, user-triggered events that communicate how a given fragment of the state should change. Middlewares are used in cases the actions require asynchronous requests.



Principles

  • The state of entire application is stored in a single location. You don't have to search across a variety of data stores to find the part of your state you want to update. Keeping everything stored in a single location also ensures you don't have to worry about keeping all of this data in-sync!

  • The state should be read-only and can only be modified through actions. You should ensure random parts of application can't access the store and modify the state stored inside it. The only way app can modify is by relying on actions.

  • You just specify what the final state should be. To keep things simple, your state is never modified or mutated. You use a reducer to just specify what the final result of your state should be. The reducer responsibility is to maintain the integrity of the data. So we can go forward or backward if we need to undo an action. There are 3 things can't be done with reducer

    • Mutate it's arguments
    • Perform side effects like API calls and routing transitions
    • Call non-pure functions

Setup

Install a bunch of external packages

npm install @ngrx/store --save
npm install @ngrx/effects --save
npm install @ngrx/router-store --save
npm install @ngrx/store-devtools --save

First, define your store and start attaching reducers to it.

The 'actions' are events. They report that something happened. They do not describe, how the state has to be changed. Every action has a type. The type is typically a string, that describes what happened. For example MouseDown. Actions can also carry a (optional) payload. This payload can be any JavaScript object.

counter.action.ts

import { Action } from '@ngrx/store';

export const ActionTypes = {
	INCREMENT: 'increment count',
	DECREMENT: 'decrement count',
	RESET: 'reset count'
};

export class ResetAction implements Action {
	public type = ActionTypes.RESET;
	constructor(public payload?: number) {
	}
}
export class IncrementAction implements Action {
	public type = ActionTypes.INCREMENT;
	constructor(public payload?: number) {
	}
}
export class DecrementAction implements Action {
	public type = ActionTypes.DECREMENT;
	constructor(public payload?: number) {
	}
}

export type Actions = ResetAction | IncrementAction | DecrementAction;

For each action that is dispatched to the reducer (or, to put it simply, every time the reducer function is called), a switch operator creates a new state, depending on the action type, with the changes applied. If the action type does not match any of the defined actions, the state is simply returned.

counter.reducer.ts

import { Action } from '@ngrx/store';
import * as counter from './counter.action';

export function counterReducer(state: number = 0, action: counter.Actions) {
	switch (action.type) {
		case counter.ActionTypes.INCREMENT:
			return (state + Number(action.payload));

		case counter.ActionTypes.DECREMENT:
			return (state - Number(action.payload));

		case counter.ActionTypes.RESET:
			return 0;

		default:
			return state;
	}
}

Next, we need to create one "Application State".

This is, because our Redux store can only consist of one JavaScript object. Also create a Meta-Reducer, which is essential for having more than one state. Meta-Reducers are a map of all the reducer functions in the state tree. It contains a reference for each of the state slices and their reducers. When an action gets dispatched, the Meta-Reducer goes through the map of all reducers, looking for the one that matches the action type and calls the function.

app.reducers.ts

import { counterReducer } from './counter.reducer';

// Interface of our application state
export interface AppState {
	counter: number;
}

// Map of all reducers, we want to register
export const reducers = {
	counter: counterReducer
};

Next, put the reducers into the store.

app.module.ts

import { NgModule } from '@angular/core'
import { StoreModule } from '@ngrx/store';
import { reducers } from './app.reducers';

@NgModule({
imports: [
	BrowserModule,
	StoreModule.forRoot(reducers)
]
})
export class AppModule {}

Next, get the state from store and dispatch the actions.

By injecting the store and using store.select(), you can access one of the states in the application. The store always returns an observable, so you have to subscribe to it, or, if you are passing it in a child component, use the async pipe.

The async pipe is a little helper angular provides. It allows us, to resolve observables directly form our template's HTML code. It takes care of all the technical difficulties, including unsubscribing from the observable.

The state of the store is updated through pre-defined actions, which are dispatched from user events and are used as input for the reducer functions. The action is dispatched using the built-in dispatch() function in the store provided by @ngrx/store.

import { Store } from '@ngrx/store';
import { AppState } from './app.reducers';
import * as counter from './counter.action';

@Component({
	selector: 'my-app',
	template: `
		<button (click)="increment()">Increment</button>
		<div>Current Count: {{ counter | async }}</div>
		<button (click)="decrement()">Decrement</button>

		<button (click)="reset()">Reset Counter</button>
	`
})
class MyAppComponent {
	counter: Observable<number>;

	constructor(private store: Store<AppState>){
		this.counter = store.select('counter');
	}

	increment(){
		this.store.dispatch(new counter.IncrementAction(1));
	}

	decrement(){
		this.store.dispatch(new counter.DecrementAction(1));
	}

	reset(){
		this.store.dispatch(new counter.ResetAction());
	}
}

Dealing with Side-Effects

Side-Effects are considered "evil" in the ngrx ecosystem. But sometimes, they can not be avoided.

The most common example of an side effect is a http-request. We make it at some point, but we don't know when it comes back. It is an asynchronous operation. To deal with this kind of side-effects, the ngrx team has created a module called ngrx/effects.

Redux handles server-side requests through a middleware - a piece of logic that stays between the server and the reducer functions. In Redux, server-side requests are regarded as side effects from actions that cannot be entirely handled through a reducer. Such actions call impure functions, making the reducers unable to handle the recreation of the action because the function does not depend entirely on its input.

The standard for handling server-side requests requires the implementation of three actions:

  • Action that indicates the start of the server-side request: This action is dispatched just before the request is made.
  • Action that indicates a successful request: This action is dispatched when te request is done. Its payload contains the request response. The reducer simply adds the response to its corresponding state property.
  • Action that indicates a failed request This action is dispatched if the request fails. The action payload may contain the error reason or simply return nothing.

Setup

Install an external package

npm install @ngrx/effects --save

Next, add a new action

counter.action.ts

export const ActionTypes = {
	MULTIPLY: 'multiply count'
};

export class MultiplyAction implements Action {
	public type = ActionTypes.MULTIPLY;
	constructor(public payload: number = 1) {
	}
}

export type Actions = ResetAction | IncrementAction | DecrementAction | MultiplyAction;

Next, we are going to implement the side-effect for handling the action. _actions.ofType() returns an observable which watches for newly dispatched events that match the type of the action.

counter.effects.ts

import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';
import * as counter from './counter.action';
import 'rxjs/add/operator/map';

@Injectable()
export class CounterEffects {
	@Effect({dispatch: false})
	multiplyCount = this._actions
				.ofType(counter.ActionTypes.MULTIPLY)
				.map((action: counter.MultiplyAction)=>{
					console.log(action);
				});

	constructor(private _actions: Actions){
	}
}

The final step for handling effects to register the effects to the app.module.

app.module.ts

import { NgModule } from '@angular/core'
import { StoreModule } from '@ngrx/store';
import { reducers } from './app.reducers';
import { EffectsModule } from '@ngrx/effects';
import { CounterEffects } from './counter.effects';

@NgModule({
imports: [
	BrowserModule,
	StoreModule.forRoot(reducers),
	EffectsModule.forRoot([CounterEffects])
]
})
export class AppModule {}

Other packages

Router-Store package

This is related to router state change which causes to change in the state of application. App simply get informed about every state change through this package.

npm install @ngrx/router-store --save
import { StoreRouterConnectingModule } from '@ngrx/router-store';

@NgModule({
	imports: [
		...
		StoreRouterConnectingModule
	]
})

Store-Devtools package

Use this package and crome browser plugin to debug our state changes whenever any action is dispatched.

npm install @ngrx/store-devtools --save
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { environment } from '../environments/environment';

@NgModule({
	imports: [
		...
		(!environment.production) ? StoreDevtoolsModule.instrument() : []
	]
})
⚠️ **GitHub.com Fallback** ⚠️