Skip to main content

Angular NGRX DevTools

Introduction

When building complex Angular applications with NGRX for state management, debugging and tracing state changes can quickly become challenging. This is where NGRX DevTools comes to the rescue. NGRX DevTools is a powerful browser extension that integrates with your Angular application, allowing you to monitor state changes, time-travel through different states, and debug your application more effectively.

In this tutorial, we'll explore how to set up NGRX DevTools in your Angular application and how to leverage its features to make state management development easier and more efficient.

Prerequisites

Before diving into NGRX DevTools, you should have:

  • Basic knowledge of Angular
  • Familiarity with NGRX and its core concepts (Store, Actions, Reducers)
  • Node.js and npm installed on your machine
  • An existing Angular project with NGRX installed

Setting Up NGRX DevTools

Step 1: Install the Browser Extension

First, you need to install the Redux DevTools extension in your browser:

Step 2: Install the NGRX DevTools Package

In your Angular project, install the @ngrx/store-devtools package using npm:

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

Step 3: Configure NGRX DevTools in Your Application

Open your app module file (typically app.module.ts) and import the StoreDevtoolsModule:

typescript
import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { environment } from '../environments/environment';
import { rootReducer } from './store/reducers';

@NgModule({
imports: [
BrowserModule,
StoreModule.forRoot(rootReducer),
StoreDevtoolsModule.instrument({
maxAge: 25, // Retains last 25 states
logOnly: environment.production, // Restrict extension to log-only mode in production
autoPause: true, // Pauses recording actions and state changes when the extension window is not open
}),
// other imports...
],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule { }

Using NGRX DevTools

Once you've set up NGRX DevTools, you can start using it to monitor and debug your application's state. Here's how:

1. Inspecting State

Open your application in the browser and launch the DevTools (F12 or right-click > Inspect). Navigate to the "Redux" tab to see the NGRX DevTools panel.

In the panel, you can see:

  • The current state of your application
  • A list of dispatched actions
  • The state changes after each action

2. Time Travel Debugging

One of the most powerful features of NGRX DevTools is time-travel debugging, which allows you to move back and forth between different states of your application:

  • Use the slider at the bottom of the panel to move between different actions
  • Click on any action in the action list to jump to that specific state
  • Use the playback controls to automatically step through actions

3. Monitoring Action Payloads

When you dispatch an action with a payload, you can inspect the payload in the DevTools:

typescript
// In your component
import { Store } from '@ngrx/store';
import { addTodo } from './store/actions';

@Component({
selector: 'app-todo',
template: `
<button (click)="addNewTodo()">Add Todo</button>
`
})
export class TodoComponent {
constructor(private store: Store) {}

addNewTodo() {
this.store.dispatch(addTodo({
id: Date.now(),
text: 'Learn NGRX DevTools',
completed: false
}));
}
}

In the DevTools, you'll see this action and its payload, making it easy to verify that the correct data is being dispatched.

4. Analyzing Action Effects

If you're using @ngrx/effects, DevTools can help you debug the flow of actions and effects:

typescript
// In your effects file
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { mergeMap, map, catchError } from 'rxjs/operators';
import { TodoService } from './todo.service';
import * as TodoActions from './todo.actions';

@Injectable()
export class TodoEffects {
loadTodos$ = createEffect(() => this.actions$.pipe(
ofType(TodoActions.loadTodos),
mergeMap(() => this.todoService.getTodos()
.pipe(
map(todos => TodoActions.loadTodosSuccess({ todos })),
catchError(error => of(TodoActions.loadTodosFailure({ error })))
))
)
);

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

In the DevTools, you can observe the chain of actions:

  1. The loadTodos action is dispatched
  2. The effect processes it and dispatches either loadTodosSuccess or loadTodosFailure
  3. The reducer processes the subsequent action and updates the state

Advanced DevTools Features

1. Custom Action Serialization

For complex actions with circular references or non-serializable data, you can customize how actions are serialized:

typescript
StoreDevtoolsModule.instrument({
maxAge: 25,
logOnly: environment.production,
actionSanitizer: (action) => {
// Custom logic to sanitize or modify actions for DevTools
return { ...action, specialField: '[SANITIZED]' };
},
stateSanitizer: (state) => {
// Custom logic to sanitize or modify state for DevTools
return { ...state, sensitiveData: '[HIDDEN]' };
}
})

2. Monitoring Specific State Slices

If you're only interested in a particular slice of your state, you can filter the state tree in DevTools:

  1. Open the DevTools panel
  2. Click on the "Inspector" tab
  3. In the "State" tree view, expand or collapse nodes to focus on specific parts of your state

3. Importing and Exporting State

NGRX DevTools allows you to save and load application states:

  1. Click on the "Export" button to save the current state to a JSON file
  2. Use "Import" to load a previously saved state, which is useful for reproducing bugs

Real-World Example: Todo Application

Let's create a simple todo application to demonstrate NGRX DevTools in action:

Step 1: Define the State Interface

typescript
// todo.model.ts
export interface Todo {
id: number;
text: string;
completed: boolean;
}

// todo.state.ts
export interface TodoState {
todos: Todo[];
loading: boolean;
error: string | null;
}

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

Step 2: Create Actions

typescript
// todo.actions.ts
import { createAction, props } from '@ngrx/store';
import { Todo } from './todo.model';

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 }>()
);

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

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

Step 3: Create Reducers

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

export const todoReducer = createReducer(
initialTodoState,
on(TodoActions.loadTodos, state => ({
...state,
loading: true
})),
on(TodoActions.loadTodosSuccess, (state, { todos }) => ({
...state,
todos,
loading: false,
error: null
})),
on(TodoActions.loadTodosFailure, (state, { error }) => ({
...state,
loading: false,
error
})),
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
)
}))
);

Step 4: Create a Todo Component

typescript
// todo.component.ts
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { Todo } from './todo.model';
import * as TodoActions from './store/todo.actions';

@Component({
selector: 'app-todo',
template: `
<div>
<h2>Todo List</h2>

<div>
<input #todoInput placeholder="Add new todo">
<button (click)="addTodo(todoInput.value); todoInput.value=''">Add</button>
</div>

<ul>
<li *ngFor="let todo of todos$ | async"
[class.completed]="todo.completed"
(click)="toggleTodo(todo.id)">
{{ todo.text }}
</li>
</ul>

<div *ngIf="loading$ | async">Loading...</div>
<div *ngIf="error$ | async as error" class="error">{{ error }}</div>
</div>
`,
styles: [`
.completed {
text-decoration: line-through;
color: gray;
}
.error {
color: red;
}
`]
})
export class TodoComponent implements OnInit {
todos$: Observable<Todo[]>;
loading$: Observable<boolean>;
error$: Observable<string | null>;

constructor(private store: Store<{ todos: TodoState }>) {
this.todos$ = this.store.select(state => state.todos.todos);
this.loading$ = this.store.select(state => state.todos.loading);
this.error$ = this.store.select(state => state.todos.error);
}

ngOnInit() {
this.store.dispatch(TodoActions.loadTodos());
}

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

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

Step 5: Add Effects

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

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

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

Using DevTools with the Todo App

Now when you run this application:

  1. Open the Redux DevTools panel
  2. You'll see the initial @ngrx/store/init action
  3. When the component loads, you'll see the [Todo] Load Todos action
  4. Followed by either [Todo] Load Todos Success or [Todo] Load Todos Failure
  5. When you add a todo, you'll see the [Todo] Add Todo action with the text in its payload
  6. When you toggle a todo, you'll see the [Todo] Toggle Todo action with the id in its payload

You can use the time-travel controls to move between states, or click on any action to jump to that state. This makes debugging significantly easier, as you can inspect how each action changes the state.

Summary

NGRX DevTools is an invaluable tool for developers working with state management in Angular applications. It provides:

  • A visual representation of your application's state
  • The ability to track action dispatches and resulting state changes
  • Time-travel debugging to move between different states
  • Tools to inspect action payloads and effects

By integrating NGRX DevTools into your development workflow, you can more easily debug complex state issues, understand state flow, and ensure your application works correctly.

Additional Resources

Exercises

  1. Basic: Add NGRX DevTools to an existing Angular application and observe state changes as you interact with the app.

  2. Intermediate: Implement a feature that uses the time-travel debugging capability to reset the application to a specific state when a "reset" button is clicked.

  3. Advanced: Create a custom reducer that logs all actions to a service when running in development mode, and use DevTools to compare your logs with the actual state transitions.

  4. Expert: Build a user settings feature where users can customize the application. Use DevTools to debug the state persistence mechanism (LocalStorage or similar) by dispatching actions that change settings and verifying the state is correctly updated.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)