When working on an Angular application, there is no out-of-the-box, quick solution for data persistence. As a result, the experience of developing a very large application that requires a lot of data communication between components can be very stressful.
In this Angular state management tutorial, we’ll show you how to use NgRx, a collection of libraries for reactive extensions, to manage state in your Angular application.
Here’s what we’ll cover:
To follow along with this tutorial, you should have:
Unlike backend applications, which use databases for state management, frontend applications need some sort of mechanism for handling data. This data can range from server responses gotten from HTTP request, to form input data and user routes.
It’s a good practice to compose all your application state in a central store for easy management and data communication. So, basically, our state is a representation of our application that actually lives in the store.
NgRx is a group of Angular libraries for reactive extensions and state management. It makes Angular development easier by simplifying the application’s state in objects and enforcing unidirectional data flow.
A complete state management system should enable you to model a state — e.g., create a simple representation of what the state should look like, update its value, monitor the state when the value changes, and retrieve the values of the state.
Libraries included in the NgRx package include:
For this tutorial, we’ll mostly focus on the Store library. Specifically, we’ll demonstrate how Angular components can get values from a single store.
The NgRx Store is a Redux-inspired state management system that enables you to use observables to manage state in an Angular application. The primary advantage to using the NgRx Store is the ability to store all state in a single tree that is accessible from any part of the application.
NgRx uses the Redux pattern, which is comprised of three main concepts:
The diagram above clearly illustrates the concept of Redux and shows how it manages state. Note that NgRx also uses this pattern.
Let’s say, for instance, we have a button in a particular component that, when clicked, changes the value of the page header. We’ll use NgRx to handle that.
First, the component dispatches an action. The action then goes to the reducer. The reducer is a simple method that takes the current state and the action as a parameter and then returns a new state. When the reducer returns the new state, the component then subscribes to a selector to get the new value.
The state is never changed directly. Instead, the reducer always creates a new state. This is known as immutability.
In this section, we’ll demonstrate how to use store states, actions, and reducers to simplify state management in Angular.
A store is basically a JavaScript object that holds data we’ll be using in our application.
A simple store takes the following format:
const state = { persons: [ { name: "Wisdom Ekpot", age: 10 }, { name: "John Cat", age: 40 }, { name: "James John", age: 11 } ], bookDescription: { name: "Name of book", author: "Wisdom Ekpot" } }
Actions are methods dispatched by the component when an event is called.
Here you can define a type and also the payload it will be sending:
const ChangeNameAction = { type: "Change Name", name: "Wisdom" }
The createReducer
method handles state transitions.
To access the state, we have to import the initial state into the reducer file. To trigger an action, we use the on
event, which takes the name of the action as a parameter:
const _reducer = createReducer( initialState, on(nameOfAction, (state, action) => { return { ...state, someState: action.anyState } }) )
Start by setting up a new Angular application. If you don’t have Angular installed on your local machine, you can open up your terminal and run npm i @angular/cli -g
. The -g
flag will ensure that the Angular CLI is installed globally on your local machine.
To create a new project, run ng new course-list
on your terminal. After the installation is complete, move into the project by running cd course-list
.
Next, install the NgRx Store into your project by running the following command in the terminal (make sure your terminal is on the project working directory):
npm install @ngrx/store --save
After installation, open up your application on VS Code and serve the application using the following command:
code . && ng serve
Create a store
directory inside your src/app
directory. This is where all the NgRx features will be added.
Inside the store
directory, create a models
directory and, inside it, a courseItem.model.ts
file. We’ll define our interface for our course list in this file:
export interface CourseItem { id: string; department: string; name: string; }
Next, create an Actions
directory inside the store
directory. This is where all the NgRx actions will be defined.
Create a course.action.ts
file inside the actions
directory and add the following:
import { Action } from '@ngrx/store'; import { CourseItem } from '../models/courseItem.model'; export enum CourseActionType { ADD_ITEM = '[COURSE] Add Course', } export class AddItemAction implements Action { readonly type = CourseActionType.ADD_ITEM; //add an optional payload constructor(public payload: CourseItem) {} } export type CourseAction = AddItemAction;
We start by importing the NgRx Action from the store and then also import the coureitem
interface that we defined. This has a lot of use cases; here, we’ll use it as a simple validation for our actions.
We’ll use the TypeScript enum to define an action that allows us to declare a set of named constants, such as a collection of related numeric or string values.
After doing this, we create an AddItemAction
, which implements the NgRx Action. Every NgRx Action has two main properties: a type and an optional payload. The type is usually a read-only string that represents the type of action we’ll be dispatching into the store. Since this is a class, we can accept an optional payload using the class constructor.
Next, we’ll export a CourseAction
type, the value of which will be the AddItemAction
action.
With this defined, we need to create our reducer to help in transitioning from stateX
to stateY
.
Let’s create a reducers
directory inside the store
directory. Inside the reducers
directory, create a course.reducer.ts
file and add the following:
// import the interface import { CourseItem } from '../models/courseItem.model'; import { CourseAction, CourseActionType } from '../actions/course.action'; //create a dummy initial state const initialState: Array<CourseItem> = [ { id: '1', department: 'Computer Engineering', name: 'C++ Programming', }, ]; export function courseReducer( state: Array<CourseItem> = initialState, action: CourseAction ) { switch (action.type) { case CourseActionType.ADD_ITEM: return [...state, action.payload]; default: return state; } }
The first thing to do is import the courseItem
model, CourseAction
action, and CourseActionType
action. For now, we’ll create an initial state that will use the CourseItem
interface for validation.
We create a reducer function that takes a state
and an action as a parameter. The state parameter will be a type of array, the value of which will be the defined initial state we just created. We use the JavaScript statement to check if the action type is defined in our actions file. If it does exist, it returns the state and the action payload. If not, it just returns the state.
Recall that the objective of NgRx or any state management system is to keep all application state in a single store so that it can be assessable from any part of the application. Let’s create a state.model.ts
file inside the models directory.
Add the following to the file you just created:
import { CourseItem } from './courseItem.model'; export interface State { readonly courses: Array<CourseItem>; }
Now we have to register NgRx in our root app.modules.ts
file. Import CourseReducer
and then register it in the imports array. We’ll also import the Angular FormsModule, which we’ll be using shortly:
import { CourseReducer } from './store/reducers/course.reducer'; import { FormsModule } from '@angular/forms';
After importing the module, register it in the imports
array:
imports: [ FormsModule, StoreModule.forRoot({ course: CourseReducer, }), ],
We can now use NgRx in our components. Let’s start by modifying our root app.component.ts
file to the following:
import { Component, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import { CourseItem } from './store/models/courseItem.model'; import { AppState } from './store/models/app-state.model'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], }) export class AppComponent implements OnInit { courseItems$: Observable<Array<CourseItem>>; constructor(private store: Store<AppState>) {} ngOnInit(): void { this.courseItems$ = this.store.select((store) => store.course); } }
We bring in RxJS Observables, our defined courseItem
interface, and our app state. We set courseItems$
to a type of observable, which will be a type of array. We then set the value of courseItems$
to the returned store.
We can now use this in our template. We’ll delete all of our template HTML in app.component.html
and convert it to the following:
<h4>Testing NgRx</h4> <ul> <li *ngFor="let course of courseItems$ | async"> {{course.department}} </li> </ul>
We can use async
to automatically subscribe to an observable and run our application using the ng serve
command. Running the application on the browser will display this:
This is returned because we haven’t called any reducer, so it just returns the state.
The next functionality we’ll tackle is adding a course to our list of courses. We’ll create a simple form for users to add a new course.
For a nice-looking user interface, we’ll use Bootstrap. We’ll add the Bootstrap CSS CDN to our app/index.html
:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>CourseList</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous"> </head> <body> <app-root></app-root> </body> </html>
To create a simple form for adding the course name and department in our user interface, let’s modify our app.component.html
file as follows:
<section> <div class="container"> <div class="row"> <div class="col-md-12"> <h4>Angular State Management</h4> </div> <div class="col-md-6"> <ul class="list-group"> <li class="list-group-item" *ngFor="let course of courseItems$ | async"> {{course.name}}: <b>{{course.department}}</b> </li> </ul> </div> <div class="col-md-6"> <div class="card p-4 shadow-sm"> <form #myform="ngForm" (ngSubmit)="addCourse(myform)"> <div class="form-group"> <label for="name">Identity</label> <input type="text" class="form-control" ngModel name="id" id="id" aria-describedby="identity" required> </div> <div class="form-group"> <label for="name">Name</label> <input type="text" class="form-control" ngModel name="name" id="name" aria-describedby="name"> </div> <div class="form-group"> <label for="department">Department</label> <input type="text" class="form-control" ngModel name="department" id="department"> </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </div> </div> </div> </div> </section>
This is a basic code refactor. Here, we’re using the Angular form ngModel to get the values of the form. Next, we need to import NgForm
in our TypeScript file and create a method to dispatch the AddItemAction
:
import { NgForm } from '@angular/forms'; import { AddItemAction } from './store/actions/course.action'; //create the method for adding a new course and then reset the form addCourse(form: NgForm) { this.store.dispatch(new AddItemAction(form.value)); form.reset(); }
We can now add a new course using the form we created:
Working with large-scale applications requires good architecture and organized structure. State management tools such as NgRx can help you maintain a readable codebase as your app scales.
Whether you’re an Angular developer or not, understanding the basic concepts behind state management will make it easier to use tools such as Redux and Vuex in your projects.
The source code for this article is available on GitHub.
Debugging Angular applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Angular state and actions for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site including network requests, JavaScript errors, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.
The LogRocket NgRx plugin logs Angular state and actions to the LogRocket console, giving you context around what led to an error, and what state the application was in when an issue occurred.
Modernize how you debug your Angular apps — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.