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:
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
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 lets you replay user sessions, eliminating guesswork by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings—compatible with all frameworks.
With Galileo AI, you can instantly identify and explain user struggles with automated monitoring of your entire product experience.
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.

:has(), with examplesThe CSS :has() pseudo-class is a powerful new feature that lets you style parents, siblings, and more – writing cleaner, more dynamic CSS with less JavaScript.

Kombai AI converts Figma designs into clean, responsive frontend code. It helps developers build production-ready UIs faster while keeping design accuracy and code quality intact.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the October 22nd issue.

John Reilly discusses how software development has been changed by the innovations of AI: both the positives and the negatives.
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 now