cyberangles guide

Introduction to NgRx: State Management in Angular

As Angular applications grow in complexity—with multiple components sharing data, handling user interactions, and managing asynchronous operations—keeping track of application state becomes increasingly challenging. State refers to the data that drives your app: user inputs, API responses, UI flags (e.g., "loading" spinners), and more. Without a structured approach, state can become scattered across components, leading to bugs, inconsistent UIs, and unmaintainable code. Enter **NgRx**—a state management library for Angular inspired by Redux and built on RxJS. NgRx provides a centralized, predictable way to manage application state, making it easier to debug, test, and scale Angular apps. In this blog, we’ll explore what NgRx is, its core concepts, how to implement it, and when to use it.

Table of Contents

  1. What is NgRx?
  2. Core Concepts of NgRx
  3. Setting Up NgRx in an Angular App
  4. Practical Example: Building a Todo App with NgRx
  5. Advanced NgRx Concepts
  6. Best Practices for Using NgRx
  7. When to Use NgRx
  8. Conclusion
  9. References

What is NgRx?

NgRx is a collection of libraries for reactive state management in Angular applications. It is modeled after Redux, a popular state management pattern, and leverages RxJS (Reactive Extensions for JavaScript) to handle asynchronous operations and state streams.

At its core, NgRx aims to address three key challenges in state management:

  • Predictability: State changes follow a strict unidirectional data flow.
  • Centralization: All application state lives in a single, immutable store.
  • Debuggability: Tools like Redux DevTools enable time-travel debugging to replay actions and inspect state changes.

NgRx consists of several packages, including:

  • @ngrx/store: The core library for managing state.
  • @ngrx/effects: Handles side effects (e.g., API calls, timers).
  • @ngrx/entity: Simplifies managing collections of data (e.g., arrays of objects).
  • @ngrx/store-devtools: Integrates with Redux DevTools for debugging.

Core Concepts of NgRx

To understand NgRx, you need to grasp its five core concepts: Store, Actions, Reducers, Selectors, and Effects. These work together to enforce the unidirectional data flow shown below:

[Component] → Dispatches [Action] → [Reducer] Updates [State] in [Store] → [Component] Selects State  

[Effects] Handle Side Effects (API calls, etc.) → Dispatch New [Actions] ┘  

Store

The Store is a centralized container that holds the entire application state. It is an observable, so components can “select” (subscribe to) specific slices of state. The store is immutable—state changes are made by dispatching actions, not by directly modifying the state.

In NgRx, the store is created using StoreModule.forRoot() (for root state) or StoreModule.forFeature() (for feature-specific state).

Actions

Actions are plain JavaScript objects that describe what happened in the application. They are the only way to trigger state changes. Every action has a type (a string identifier) and an optional payload (data associated with the action).

Actions follow a convention: [Source] Action Name. For example:

// src/app/store/todo.actions.ts  
export const addTodo = createAction(  
  '[Todo Page] Add Todo',  
  props<{ text: string }>() // Payload: { text: string }  
);  

export const toggleTodo = createAction(  
  '[Todo Page] Toggle Todo',  
  props<{ id: number }>()  
);  

Here, createAction (from @ngrx/store) simplifies action creation by defining the type and payload.

Reducers

Reducers are pure functions that take the current state and an action, then return a new state. They specify how the state changes in response to actions.

Key rules for reducers:

  • They must be pure (no side effects, same input → same output).
  • They must never mutate the existing state—always return a new state object.

Example: Define a state interface and reducer for a todo app:

// src/app/store/todo.state.ts  
export interface Todo {  
  id: number;  
  text: string;  
  completed: boolean;  
}  

export interface TodoState {  
  todos: Todo[];  
  loading: boolean;  
  error: string | null;  
}  

// Initial state  
export const initialState: TodoState = {  
  todos: [],  
  loading: false,  
  error: null  
};  

// Reducer  
import { createReducer, on } from '@ngrx/store';  
import * as TodoActions from './todo.actions';  

export const todoReducer = createReducer(  
  initialState,  
  on(TodoActions.addTodo, (state, { text }) => {  
    const newTodo: Todo = {  
      id: Date.now(), // Simple ID generation  
      text,  
      completed: false  
    };  
    return { ...state, todos: [...state.todos, newTodo] }; // Immutable update  
  }),  
  on(TodoActions.toggleTodo, (state, { id }) => ({  
    ...state,  
    todos: state.todos.map(todo =>  
      todo.id === id ? { ...todo, completed: !todo.completed } : todo  
    )  
  }))  
);  

Here, createReducer and on (from @ngrx/store) simplify defining how actions modify state.

Selectors

Selectors are pure functions that extract specific slices of state from the store. They are memoized (cached) to avoid redundant computations, improving performance.

Use createSelector and createFeatureSelector (for feature states) to create selectors:

// src/app/store/todo.selectors.ts  
import { createFeatureSelector, createSelector } from '@ngrx/store';  
import { TodoState } from './todo.state';  

// Select the feature state (if using feature modules)  
export const selectTodoState = createFeatureSelector<TodoState>('todos');  

// Select all todos  
export const selectAllTodos = createSelector(  
  selectTodoState,  
  (state: TodoState) => state.todos  
);  

// Select completed todos (derived state)  
export const selectCompletedTodos = createSelector(  
  selectAllTodos,  
  (todos: Todo[]) => todos.filter(todo => todo.completed)  
);  

Components use selectors to access state:

// In a component  
todos$ = this.store.select(selectAllTodos);  
completedTodos$ = this.store.select(selectCompletedTodos);  

Effects

Effects handle side effects (e.g., API calls, timers, logging) that occur in response to actions. They listen to actions, perform async operations, and dispatch new actions based on the result.

Effects are defined using createEffect and return an observable of actions:

// src/app/store/todo.effects.ts  
import { Injectable } from '@angular/core';  
import { Actions, createEffect, ofType } from '@ngrx/effects';  
import { of } from 'rxjs';  
import { catchError, map, mergeMap } from 'rxjs/operators';  
import { TodoService } from '../services/todo.service';  
import * as TodoActions from './todo.actions';  

@Injectable()  
export class TodoEffects {  
  loadTodos$ = createEffect(() =>  
    this.actions$.pipe(  
      ofType(TodoActions.loadTodos), // Listen for "loadTodos" action  
      mergeMap(() =>  
        this.todoService.getTodos().pipe(  
          map(todos => TodoActions.loadTodosSuccess({ todos })), // Dispatch success action  
          catchError(error => of(TodoActions.loadTodosFailure({ error: error.message }))) // Dispatch failure action  
        )  
      )  
    )  
  );  

  constructor(private actions$: Actions, private todoService: TodoService) {}  
}  

Here, loadTodos$ is an effect that triggers when loadTodos is dispatched. It calls TodoService.getTodos(), then dispatches loadTodosSuccess or loadTodosFailure.

Setting Up NgRx in an Angular App

Follow these steps to add NgRx to an Angular project:

Step 1: Install Dependencies

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

Step 2: Configure the Store

In app.module.ts, import StoreModule and StoreDevtoolsModule (for debugging):

// src/app/app.module.ts  
import { NgModule } from '@angular/core';  
import { BrowserModule } from '@angular/platform-browser';  
import { StoreModule } from '@ngrx/store';  
import { StoreDevtoolsModule } from '@ngrx/store-devtools';  
import { todoReducer } from './store/todo.reducer';  

@NgModule({  
  imports: [  
    BrowserModule,  
    StoreModule.forRoot({ todos: todoReducer }), // Register root reducers  
    StoreDevtoolsModule.instrument({ maxAge: 25 }) // Enable DevTools  
  ],  
  declarations: [AppComponent],  
  bootstrap: [AppComponent]  
})  
export class AppModule {}  

Step 3: (Optional) Use Feature Modules

For large apps, split state into feature modules using StoreModule.forFeature():

// src/app/features/todos/todo.module.ts  
import { NgModule } from '@angular/core';  
import { StoreModule } from '@ngrx/store';  
import { EffectsModule } from '@ngrx/effects';  
import { todoReducer } from './store/todo.reducer';  
import { TodoEffects } from './store/todo.effects';  

@NgModule({  
  imports: [  
    StoreModule.forFeature('todos', todoReducer), // Feature state key: "todos"  
    EffectsModule.forFeature([TodoEffects]) // Register feature effects  
  ]  
})  
export class TodoModule {}  

Practical Example: Building a Todo App with NgRx

Let’s build a simple todo app to tie together the concepts above.

1. Define the State Interface

// src/app/store/todo.state.ts  
export interface Todo {  
  id: number;  
  text: string;  
  completed: boolean;  
}  

export interface TodoState {  
  todos: Todo[];  
  loading: boolean;  
  error: string | null;  
}  

export const initialState: TodoState = {  
  todos: [],  
  loading: false,  
  error: null  
};  

2. Create Actions

// src/app/store/todo.actions.ts  
import { createAction, props } from '@ngrx/store';  
import { Todo } from './todo.state';  

// Load todos  
export const loadTodos = createAction('[Todo] Load Todos');  
export const loadTodosSuccess = createAction(  
  '[Todo] Load Todos Success',  
  props<{ todos: Todo[] }>()  
);  
export const loadTodosFailure = createAction(  
  '[Todo] Load Todos Failure',  
  props<{ error: string }>()  
);  

// Add todo  
export const addTodo = createAction(  
  '[Todo] Add Todo',  
  props<{ text: string }>()  
);  

// Toggle todo  
export const toggleTodo = createAction(  
  '[Todo] Toggle Todo',  
  props<{ id: number }>()  
);  

3. Write the Reducer

// src/app/store/todo.reducer.ts  
import { createReducer, on } from '@ngrx/store';  
import { initialState, TodoState } from './todo.state';  
import * as TodoActions from './todo.actions';  

export const todoReducer = createReducer(  
  initialState,  
  on(TodoActions.loadTodos, state => ({ ...state, loading: true, error: null })),  
  on(TodoActions.loadTodosSuccess, (state, { todos }) => ({  
    ...state,  
    todos,  
    loading: false  
  })),  
  on(TodoActions.loadTodosFailure, (state, { error }) => ({  
    ...state,  
    error,  
    loading: false  
  })),  
  on(TodoActions.addTodo, (state, { text }) => ({  
    ...state,  
    todos: [...state.todos, { id: Date.now(), text, completed: false }]  
  })),  
  on(TodoActions.toggleTodo, (state, { id }) => ({  
    ...state,  
    todos: state.todos.map(todo =>  
      todo.id === id ? { ...todo, completed: !todo.completed } : todo  
    )  
  }))  
);  

4. Create Selectors

// src/app/store/todo.selectors.ts  
import { createFeatureSelector, createSelector } from '@ngrx/store';  
import { TodoState } from './todo.state';  

export const selectTodoState = createFeatureSelector<TodoState>('todos');  
export const selectAllTodos = createSelector(selectTodoState, (state) => state.todos);  
export const selectLoading = createSelector(selectTodoState, (state) => state.loading);  

5. Component Usage

Dispatch actions and select state in a component:

// src/app/todo-page/todo-page.component.ts  
import { Component, OnInit } from '@angular/core';  
import { Store } from '@ngrx/store';  
import { Observable } from 'rxjs';  
import { Todo } from '../store/todo.state';  
import * as TodoActions from '../store/todo.actions';  
import { selectAllTodos, selectLoading } from '../store/todo.selectors';  

@Component({  
  selector: 'app-todo-page',  
  template: `  
    <h2>Todos</h2>  
    <input #todoText type="text" placeholder="Add todo...">  
    <button (click)="addTodo(todoText.value)">Add</button>  

    <div *ngIf="loading$ | async">Loading...</div>  
    <ul>  
      <li *ngFor="let todo of todos$ | async"  
          (click)="toggleTodo(todo.id)"  
          [style.textDecoration]="todo.completed ? 'line-through' : 'none'">  
        {{ todo.text }}  
      </li>  
    </ul>  
  `  
})  
export class TodoPageComponent implements OnInit {  
  todos$: Observable<Todo[]>;  
  loading$: Observable<boolean>;  

  constructor(private store: Store) {  
    this.todos$ = this.store.select(selectAllTodos);  
    this.loading$ = this.store.select(selectLoading);  
  }  

  ngOnInit(): void {  
    this.store.dispatch(TodoActions.loadTodos()); // Load todos on init  
  }  

  addTodo(text: string): void {  
    if (text.trim()) {  
      this.store.dispatch(TodoActions.addTodo({ text }));  
    }  
  }  

  toggleTodo(id: number): void {  
    this.store.dispatch(TodoActions.toggleTodo({ id }));  
  }  
}  

Advanced NgRx Concepts

NgRx Entity

Managing collections (e.g., arrays of todos) can be error-prone (e.g., updating, deleting, or finding items). NgRx Entity simplifies this with a utility called EntityState, which normalizes state into a dictionary (e.g., { ids: [1, 2], entities: { 1: { id: 1, ... }, 2: { id: 2, ... } } }).

Example using EntityState:

// src/app/store/todo.state.ts  
import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';  

export interface Todo { id: number; text: string; completed: boolean; }  

// EntityState for Todo  
export interface TodoState extends EntityState<Todo> {  
  loading: boolean;  
  error: string | null;  
}  

// Create an adapter for CRUD operations  
export const todoAdapter: EntityAdapter<Todo> = createEntityAdapter<Todo>();  

// Initial state using the adapter  
export const initialState: TodoState = todoAdapter.getInitialState({  
  loading: false,  
  error: null  
});  

Now reducers can use adapter methods like addOne, updateOne, or removeOne:

// In todo.reducer.ts  
on(TodoActions.addTodo, (state, { text }) => {  
  const todo = { id: Date.now(), text, completed: false };  
  return todoAdapter.addOne(todo, state); // Add todo to the entity state  
});  

Router Store

NgRx Router Store syncs the Angular router state with the NgRx store. This allows you to select router state (e.g., URL, params) and dispatch router actions (e.g., go, back) from effects or components.

Setup:

npm install @ngrx/router-store --save  

Configure in app.module.ts:

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

@NgModule({  
  imports: [  
    // ...  
    StoreRouterConnectingModule.forRoot() // Sync router state  
  ]  
})  
export class AppModule {}  

Select router state:

import { getSelectors, RouterReducerState } from '@ngrx/router-store';  

export const selectRouterState = createFeatureSelector<RouterReducerState>('router');  
export const { selectCurrentRoute } = getSelectors(selectRouterState);  

// In a component  
currentRoute$ = this.store.select(selectCurrentRoute);  

Best Practices for Using NgRx

To avoid common pitfalls, follow these best practices:

  1. Keep Reducers Pure: No side effects, API calls, or state mutations.
  2. Normalize State Shape: Use NgRx Entity or dictionaries to avoid nested/duplicated data.
  3. Use Feature Modules: Split state into feature modules for large apps (e.g., todos, users).
  4. Avoid Over-Centralization: Not all state needs to be in NgRx. Local component state (e.g., form inputs) can stay in the component.
  5. Leverage Selectors: Always use selectors to access state—never select directly from the store in components.
  6. Use DevTools: Debug with Redux DevTools to trace actions and state changes.

When to Use NgRx

NgRx adds complexity, so use it only when necessary:

  • Large Apps: Multiple teams or components sharing state.
  • Complex State Interactions: State depends on multiple sources or async operations.
  • Debugging Needs: Time-travel debugging or audit trails are required.
  • Team Collaboration: Enforcing state management patterns across a team.

For small apps or apps with simple state, NgRx may introduce unnecessary overhead.

Conclusion

NgRx is a powerful tool for managing state in large, scalable Angular applications. By enforcing unidirectional data flow and centralizing state, it improves predictability and debuggability. Its core concepts—Store, Actions, Reducers, Selectors, and Effects—work together to handle everything from simple state updates to complex async workflows.

While NgRx adds boilerplate, the benefits of maintainability and scalability make it worth adopting for large projects. Start small (e.g., a todo app), experiment with the DevTools, and gradually integrate it into your workflow.

References