Table of contents
1.
Introduction
2.
What is NgRx?
2.1.
Features of NgRx
2.2.
Benefits of Using NgRx
3.
How NgRx Works
3.1.
NgRx Flow
3.2.
Visual Representation
4.
How to Use NgRx Store with Angular
4.1.
Step 1: Install NgRx
4.2.
Step 2: Define the State
4.3.
Step 3: Create Actions
4.4.
Step 4: Create a Reducer
4.5.
Step 5: Register the Store
4.6.
Step 6: Use Selectors in Components
4.7.
Step 7: Handle Side Effects with Effects
5.
Simple Service-Based State Management  
5.1.
Angular Signals: The New State Management Approach  
6.
Best Practices & Recommendations  
7.
Frequently Asked Questions
7.1.
What is the purpose of NgRx in Angular applications?
7.2.
What are the core components of NgRx?
7.3.
Can NgRx be used with small applications?
8.
Conclusion
Last Updated: Jan 19, 2025
Medium

State Management in Angular

Author Gaurav Gandhi
0 upvote
Career growth poll
Do you think IIT Guwahati certified course can help you in your career?

Introduction

State Management in Angular refers to the process of handling and maintaining the data (state) used across different components in an Angular application. It ensures that data changes in one part of the app are reflected seamlessly in other parts, providing a consistent user experience. State management is essential for building scalable and dynamic applications, especially when dealing with complex data flows or shared data.

State Management in Angular

This article discusses NgRx, a popular state management in Angular, and explains how it works, along with practical examples. 

What is NgRx?

NgRx is a reactive state management library for Angular applications. It is built using RxJS and adheres to the Redux pattern, which promotes unidirectional data flow. NgRx helps developers manage complex application states by centralizing state and actions in a predictable way.

Features of NgRx

  1. Centralized State Management: All the application states are stored in a single location, making it easier to debug and test.
     
  2. Predictable State Changes: State updates are managed using actions and reducers, ensuring a predictable data flow.
     
  3. Reactive Programming: Utilizes RxJS observables to handle asynchronous tasks efficiently.
     
  4. Immutability: State is treated as immutable, ensuring that changes do not occur directly but through defined actions.

Benefits of Using NgRx

  • Simplifies state management for large-scale applications.
     
  • Enhances scalability and maintainability.
     
  • Provides powerful debugging tools like Redux DevTools.

How NgRx Works

NgRx works by breaking down the application’s state management into several key components:

  1. Store: A centralized location that holds the state of the application.
     
  2. Actions: Objects that describe an event or change in the application’s state.
     
  3. Reducers: Pure functions that specify how the state should change in response to an action.
     
  4. Selectors: Functions used to retrieve specific slices of the state.
     
  5. Effects: Handle side effects such as API calls or other asynchronous operations.

NgRx Flow

  1. Dispatch an Action: Actions are dispatched to indicate that something has occurred in the application.
     
  2. Reducer Updates the State: The reducer listens for the action and updates the state accordingly.
     
  3. Selectors Retrieve Data: Components use selectors to get the required data from the store.
     
  4. Effects Perform Side Effects: For tasks like fetching data from an API, effects are triggered and return new actions.

Visual Representation

Action -> Reducer -> Store -> Component
        -> Effect -> API -> Store

How to Use NgRx Store with Angular

Now let’s go step by step to set up and use the NgRx store in an Angular application.

Step 1: Install NgRx

First, install the required NgRx packages using npm:

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

Step 2: Define the State

Create an interface to define the shape of your state. For example, for a simple todo application:

export interface Todo {
  id: number;
  title: string;
  completed: boolean;
}
export interface AppState {
  todos: Todo[];
}

Step 3: Create Actions

Define actions to describe changes in the state. Use createAction from NgRx:

import { createAction, props } from '@ngrx/store';
export const addTodo = createAction(
  '[Todo] Add Todo',
  props<{ todo: Todo }>()
);
export const removeTodo = createAction(
  '[Todo] Remove Todo',
  props<{ id: number }>()
);
export const loadTodos = createAction('[Todo] Load Todos');

Step 4: Create a Reducer

Reducers specify how the state changes based on actions:

import { createReducer, on } from '@ngrx/store';
import { addTodo, removeTodo, loadTodos } from './todo.actions';
import { Todo } from './todo.state';
const initialState: Todo[] = [];
export const todoReducer = createReducer(
  initialState,
  on(addTodo, (state, { todo }) => [...state, todo]),
  on(removeTodo, (state, { id }) => state.filter(todo => todo.id !== id))
);

Step 5: Register the Store

Add the reducer to the StoreModule in your Angular application:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { StoreModule } from '@ngrx/store';
import { todoReducer } from './todo.reducer';
@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    StoreModule.forRoot({ todos: todoReducer })
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Step 6: Use Selectors in Components

Use selectors to retrieve data from the store in a component:

import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState, Todo } from './todo.state';
@Component({
  selector: 'app-root',
  template: `
    <ul>
      <li *ngFor="let todo of todos$ | async">
        {{ todo.title }}
      </li>
    </ul>
  `
})
export class AppComponent {
  todos$ = this.store.select(state => state.todos);

  constructor(private store: Store<AppState>) {}
}

Step 7: Handle Side Effects with Effects

Use effects for asynchronous operations like fetching data:

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { loadTodos } from './todo.actions';
import { of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';

@Injectable()
export class TodoEffects {
  loadTodos$ = createEffect(() =>
    this.actions$.pipe(
      ofType(loadTodos),
      switchMap(() => of([{ id: 1, title: 'Learn NgRx', completed: false }])),
      map(todos => ({ type: '[Todo API] Todos Loaded', todos }))
    )
  );

  constructor(private actions$: Actions) {}
}

Simple Service-Based State Management  

State management in Angular can be done in many ways, & one of the simplest methods is using Angular services. Services in Angular are singleton objects, meaning they are created once & shared across the entire application. This makes them perfect for managing shared state.  

Let’s say you are building a to-do list application. You need to store & manage the list of tasks in one place so that all components can access & update it. This is where a service becomes very useful.  

Step 1: Create a Service  

First, generate a service using Angular CLI:  

ng generate service todo


This will create a `todo.service.ts` file.  


Step 2: Define the State in the Service  

Inside the service, you can define a variable to store the state. For example, let’s create an array to store the to-do tasks:  

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class TodoService {
  private tasks: string[] = [];

  getTasks(): string[] {
    return this.tasks;
  }

  addTask(task: string): void {
    this.tasks.push(task);
  }

  removeTask(index: number): void {
    this.tasks.splice(index, 1);
  }
}


Here, `tasks` is the state. The `getTasks`, `addTask`, & `removeTask` methods are used to read & update the state.  


Step 3: Use the Service in Components  

Now, let’s use this service in a component. First, generate a component:  

ng generate component todo-list


In the `todo-list.component.ts` file, inject the service & use it to manage the state:  

import { Component } from '@angular/core';
import { TodoService } from '../todo.service';


@Component({
  selector: 'app-todo-list',
  templateUrl: './todo-list.component.html',
  styleUrls: ['./todo-list.component.css'],
})
export class TodoListComponent {
  tasks: string[] = [];
  newTask: string = '';

  constructor(private todoService: TodoService) {}

  ngOnInit(): void {
    this.tasks = this.todoService.getTasks();
  }

  addTask(): void {
    if (this.newTask.trim()) {
      this.todoService.addTask(this.newTask);
      this.tasks = this.todoService.getTasks();
      this.newTask = '';
    }
  }
  removeTask(index: number): void {
    this.todoService.removeTask(index);
    this.tasks = this.todoService.getTasks();
  }
}


In the `todo-list.component.html` file, display the tasks & provide a way to add/remove them:  

<div>
  <input [(ngModel)]="newTask" placeholder="Add a new task" />
  <button (click)="addTask()">Add</button>
</div>
<ul>
  <li ngFor="let task of tasks; let i = index">
    {{ task }} <button (click)="removeTask(i)">Remove</button>
  </li>
</ul>


Explanation  

  • The `TodoService` holds the state (`tasks` array) & provides methods to update it.  
     
  • The `TodoListComponent` uses the service to read & modify the state.  
     
  • This approach is simple & works well for small applications.  

Angular Signals: The New State Management Approach  

Angular recently introduced a new feature called Signals to make state management more efficient & reactive. Signals are a way to track changes in data & automatically update the UI when the data changes. They are designed to be simple, lightweight, & highly performant.  

Unlike traditional state management methods, Signals provide a more declarative way to handle state. They are especially useful for managing complex state in large applications.  

Step 1: Understanding Signals  

A Signal is a wrapper around a value that can notify interested consumers when that value changes. You can create a Signal using the `signal` function & update it using the `set` or `update` methods.  


Step 2: Creating a Signal  

Let’s continue with the to-do list example. First, update your Angular project to the latest version to use Signals (if not already done).  

In the `todo.service.ts` file, replace the old state management with Signals:  

import { Injectable, signal } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class TodoService {
  private tasks = signal<string[]>([]);

  getTasks() {
    return this.tasks();
  }

  addTask(task: string): void {
    this.tasks.update((currentTasks) => [...currentTasks, task]);
  }

  removeTask(index: number): void {
    this.tasks.update((currentTasks) => {
      const updatedTasks = [...currentTasks];
      updatedTasks.splice(index, 1);
      return updatedTasks;
    });
  }
}


Here, `tasks` is now a Signal. The `signal` function initializes it with an empty array. The `update` method is used to modify the Signal’s value.  


Step 3: Using Signals in Components  

In the `todo-list.component.ts` file, update the component to use Signals:  

import { Component } from '@angular/core';
import { TodoService } from '../todo.service';

@Component({
  selector: 'app-todo-list',
  templateUrl: './todo-list.component.html',
  styleUrls: ['./todo-list.component.css'],
})
export class TodoListComponent {
  tasks = this.todoService.getTasks();
  newTask: string = '';

  constructor(private todoService: TodoService) {}

  addTask(): void {
    if (this.newTask.trim()) {
      this.todoService.addTask(this.newTask);
      this.newTask = '';
    }
  }

  removeTask(index: number): void {
    this.todoService.removeTask(index);
  }
}


In the `todo-list.component.html` file, bind the Signal directly to the template:  

<div>
  <input [(ngModel)]="newTask" placeholder="Add a new task" />
  <button (click)="addTask()">Add</button>
</div>

<ul>
  <li ngFor="let task of tasks; let i = index">
    {{ task }} <button (click)="removeTask(i)">Remove</button>
  </li>
</ul>


Explanation  

  • Signals automatically notify the template when the state changes, so you don’t need to manually update the UI.  
     
  • The `update` method ensures immutability, which is crucial for maintaining predictable state.  
     
  • Signals are more efficient than traditional methods because they only update the parts of the UI that depend on the changed state.  

Best Practices & Recommendations  

State management is a crucial part of building scalable & maintainable Angular applications. While Angular provides multiple ways to handle state, it’s important to follow best practices to avoid common pitfalls. Let’s discuss some of the recommendations to help you manage state effectively:  

1. Keep State Centralized  

Centralizing the state makes it easier to manage & debug. Instead of scattering state across multiple components, store it in a service or a dedicated state management library like NgRx or Akita.  

For example, in our to-do list application, we used a service (`TodoService`) to centralize the state. This ensures that all components access & modify the same state.  
 

2. Use Immutable Data Structures  

Immutable data structures prevent accidental changes to the state, making it easier to track changes & debug issues. In our Signal example, we used the `update` method to ensure immutability:  

this.tasks.update((currentTasks) => [...currentTasks, task]);


This creates a new array instead of modifying the existing one.  

 

3. Avoid Overusing Signals  

While signals are powerful, they are not always necessary. For simple state management, services or even component-level state might be sufficient. Use Signals when you need fine-grained reactivity or have complex state dependencies.  


4. Leverage Angular’s Built-in Features  

Angular provides built-in features like `@Input`, `@Output`, & `ngOnChanges` for component communication. Use these features for simple parent-child component interactions before introducing a state management library.  


5. Use a State Management Library for Complex Applications  

For large applications with complex state, consider using a state management library like NgRx or Akita. These libraries provide tools like actions, reducers, & effects to manage state in a predictable way.  


6. Optimize Performance  

State management can impact performance if not done carefully. Use Angular’s `ChangeDetectionStrategy.OnPush` to reduce unnecessary change detection cycles. For example:  

import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
  selector: 'app-todo-list',
  templateUrl: './todo-list.component.html',
  styleUrls: ['./todo-list.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TodoListComponent {
  // Component logic here
}


This ensures that the component only updates when its inputs change.  

7. Write Tests for State Management  

State management logic should be thoroughly tested to ensure it works as expected. Use Angular’s testing utilities to write unit tests for your services & components.  

For example, here’s a simple test for the `TodoService`:  

import { TestBed } from '@angular/core/testing';
import { TodoService } from './todo.service';

describe('TodoService', () => {
  let service: TodoService;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(TodoService);
  });

  it('should add a task', () => {
    service.addTask('Learn Angular');
    expect(service.getTasks()).toContain('Learn Angular');
  });

  it('should remove a task', () => {
    service.addTask('Learn Angular');
    service.removeTask(0);
    expect(service.getTasks()).not.toContain('Learn Angular');
  });
});


8. Document Your State Management Strategy  

Documenting how state is managed in your application helps new developers understand the codebase quickly. Include details like where the state is stored, how it’s updated, & which components depend on it.  

Frequently Asked Questions

What is the purpose of NgRx in Angular applications?

NgRx provides a predictable state management solution, making it easier to handle complex state changes, asynchronous operations, and debugging.

What are the core components of NgRx?

The main components of NgRx are:

  • Store: Holds the state.
     
  • Actions: Define events.
     
  • Reducers: Modify the state.
     
  • Selectors: Retrieve state slices.
     
  • Effects: Handle side effects.

Can NgRx be used with small applications?

Yes, but it is generally more useful for medium to large-scale applications where managing state manually can become cumbersome.

Conclusion

In this article, we discussed state management in Angular using NgRx. We covered the basics of NgRx, how it works, and instructions for integrating it into an Angular application. With examples of actions, reducers, and effects, we can now implement state management effectively in our projects. 

You can also check out our other blogs on Code360.

Live masterclass