banjocode How To Setup NgRx in an Angular Application

How To Setup NgRx in an Angular Application

Setup state management with NgRx in your Angular application.

15 min read

State management with NgRx

NgRx uses the redux pattern within an Angular application. Setting it up can be quite difficult, so this is a guide following some of the best practices in setting up state management with NgRx.

Installation

First, we need to install the necessary dependencies. As we’ll go for a solid base, we will use the store, effects and store-devtools.

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

Configure app module

Let’s configure app.module.ts to add the NgRx to our application.

// import
import { StoreModule } from "@ngrx/store";
import { StoreDevtoolsModule } from "@ngrx/store-devtools";
import { environment } from "src/environments/environment";
import { EffectsModule } from "@ngrx/effects";

//...
@NgModule({
    //...
    imports: [
        // ...
        StoreModule.forRoot({}),
		EffectsModule.forRoot([]),
		StoreDevtoolsModule.instrument({
			maxAge: 25,
			logOnly: environment.production
		})
    ]

})

So basically, what we do is add an empty object to the StoreModule as well as an empty array to the EffectsModule, we will declare more in the store within a feature later on. We also add the StoreDevtoolsModule if you want to use the Redux DevTools while debugging your application.

Configure sub-module

We will be declaring our reducer in a sub-module, to be able to use lazy loading. For this example, we will use it in a sub-module called dashboard.module.ts.

// imports
import { StoreModule } from "@ngrx/store";
import { reducer } from "../../state/company/company.reducer";
import { EffectsModule } from "@ngrx/effects";
import { CompanyEffects } from "src/app/state/company/company.effects";

//...
@NgModule({
    //...
    imports: [
        // ...
		StoreModule.forFeature("companies", reducer),
		EffectsModule.forFeature([CompanyEffects])
    ]
})

In this example, we will be working with a feature in the StoreModule. This part will handle companies data, that is why the name is companies. All imports are from files that will be created soon, like the reducer and CompanyEffects.

File location

Let’s create a state folder within the app folder. We will add all the necessary folders directly. In the end, it will look like this.

folder structure

NgRx uses actions, effects and reducers to manage state. I won’t go in to depth on what each one of them does for now - this will simply show how to set it up.

State

Let’s create a standard state within the app.state.ts file. As we currently do not have anything to include besides the companies, we can just leave it empty.

export interface State {}

Reducer

The reducer is probably the most important, and it will include quite a lot. Let’s begin by importing the state we already made, add companies to it, as well as create a simple reducer. This is in the company.reducer.ts file.

import * as fromRoot from "../app.state";

export interface State extends fromRoot.State {
	companies: CompanyState;
}

export interface CompanyState {
	companies: Company[]; // company is for now, only a made up interface. It can be anything.
}

export function reducer(state: CompanyState, action: any) {
	switch (action.type) {
		default:
			return state;
	}
}

So a lot is going on here. We import the empty state we defined earlier. We overwrite it and add the companies property, in which we will have the CompanyState. The company state will include a list of type Company, which we haven’t declared. This is just as an example.

The store could now look something like this, if we would populate it:

store {
    companies: {
        [
            company1: {},
            company2: {},
            //...
        ]
    }
}

However, we created an empty initialState for now.

Actions

Let’s add the actions in the company.action.ts.

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

export enum CompanyActionTypes {
	UpdateCompanies = "[Company] Update All Companies",
	UpdateSingleCompany = "[Company] Update Single Company",
}

export class UpdateCompanies implements Action {
	readonly type = CompanyActionTypes.UpdateCompanies;

	constructor(public payload: Company[]) {}
}

export class UpdateSingleCompany implements Action {
	readonly type = CompanyActionTypes.UpdateSingleCompany;

	constructor(public payload: Company) {}
}

export type CompanyActions =
	| UpdateCompanies
	| UpdateSingleCompany

We have defined two actions. One for updating the whole array of companies, and one for updating only one company. These will be used back in our reducer to be able to strongly type our actions.

Adding actions to the reducer

We will add our actions to the reducer we just created.

//...
import { CompanyActions, CompanyActionTypes } from "./company.actions";

const initialState: CompanyState = [
	{
		id: "",
		name: "",
	},
];

// ...
export function reducer(state = initialState, action: CompanyActions) {
	switch (action.type) {
		case CompanyActionTypes.UpdateCompanies:
			return { ...state, companies: action.payload };

		case CompanyActionTypes.UpdateSingleCompany:
			const updatedCompany = action.payload;

			const dataWithoutCompany = state.filter(
				(company) => company.id !== updatedCompany.id
			);

			const dataWithNewCompany = [...dataWithoutCompany, updatedCompany];
			return { ...state, companies: dataWithNewCompany };

		default:
			return state;
	}
}

We updated our reducer with the two cases - one for each action. We also use the spread operator to merge our current state with our newly updated companies state. UpdateCompanies replaces everything. While UpdateSingleCompany in this case uses the ID to replace the single company, and updates the state with the new array. I also added an initialState, which will be the base whatever the application start, until new ones are loaded. This case, it is just an array with one, empty object.

Selectors

We use selectors to get the data from our state within our components. Let’s declare them in company.reducer.ts.

// ...
import { createFeatureSelector, createSelector } from "@ngrx/store";

// ...
const getCompanyFeatureState =
	createFeatureSelector < CompanyState > "companies";

export const getCompanies = createSelector(
	getCompanyFeatureState,
	(state) => state.companies
);

export const getSingleCompany = createSelector(
	getCompanyFeatureState,
	(state: CompanyState, props: { id: string }) => {
		const stateAsArray = Object.values(state); // turn into an array
		return stateAsArray.find((company) => company.companyId === props.id);
	}
);

// ...

We created two selectors, one to get all companies, and one to get a single company based on a provided ID.

Using selectors and actions in components

These are some examples how you use the selectors and actions to manage your state within your components.

// ...
import { Store, select } from "@ngrx/store";
import * as fromCompany from "../../../../../state/company/company.reducer";
import * as companyActions from "../../../../../state/company/company.actions";

constructor(private store: Store<fromCompany.State>) {}

ngOnInit() {
    // get all companies
    this.store
        .pipe(select(fromCompany.getCompanies))
        .subscribe(companies =>
            console.log("These are all companies: ", companies)
        );

    // get single company
    this.store
		.select(fromCompany.getSingleCompany, { id: "1234" })
        .subscribe(company => console.log("Single company: ", company));

    // Update all companies
    this.store.dispatch(new companyActions.UpdateCompanies(this.listOfCompanies));
}

Effects

Effects are often used to load the data, for example from an API endpoint.

Firstly, let’s create new actions for load, and load success (I won’t cover when the load fails). Add these actions to company.action.ts.

// ...
export class Load implements Action {
	readonly type = CompanyActionTypes.Load;
}

export class LoadSuccess implements Action {
	readonly type = CompanyActionTypes.LoadSuccess;

	constructor(public payload: Company[]) {}
}

export type CompanyActions =
	| UpdateCompanies
	| UpdateSingleCompany
	| Load
	| LoadSuccess

Let’s add this to the company.effects.ts.

import { Injectable } from "@angular/core";
import { Actions, Effect, ofType } from "@ngrx/effects";

import * as companyAction from "./company.actions";
import { CompanyService } from "src/app/core/service/company.service";
import { mergeMap, map } from "rxjs/operators";
import { from } from "rxjs";


@Injectable()
export class CompanyEffects {
	constructor(
		private actions$: Actions,
		private companyService: CompanyService
	) {}

	@Effect()
	loadCompanies$ = this.actions$.pipe(
		ofType(companyAction.CompanyActionTypes.Load),
		mergeMap((action: companyAction.Load) =>
			this.companyService.getCompanies()).pipe(
				map((companies: Company[]) => {
					return new companyAction.LoadSuccess(companies);
				})
		)
	);
}

What this basically does is return the LoadSuccess action with all companies. In this case, getCompanies returns an observable with the companies.

Finally, let’s add load success to our reducer, which is basically the same as updating all companies.

// ...
export function reducer(state = initialState, action: CompanyActions) {
	switch (action.type) {
		// ...
		case CompanyActionTypes.LoadSuccess:
			return { ...state, companies: action.payload };

		default:
			return state;
	}
}

We call the load service like this:

this.store.dispatch(new companyActions.Load());