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:
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
:
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:
// 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:
// 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:
- The
loadTodos
action is dispatched - The effect processes it and dispatches either
loadTodosSuccess
orloadTodosFailure
- 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:
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:
- Open the DevTools panel
- Click on the "Inspector" tab
- 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:
- Click on the "Export" button to save the current state to a JSON file
- 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
// 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
// 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
// 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
// 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
// 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:
- Open the Redux DevTools panel
- You'll see the initial
@ngrx/store/init
action - When the component loads, you'll see the
[Todo] Load Todos
action - Followed by either
[Todo] Load Todos Success
or[Todo] Load Todos Failure
- When you add a todo, you'll see the
[Todo] Add Todo
action with the text in its payload - 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
- Official NGRX DevTools Documentation
- Redux DevTools Extension Documentation
- NGRX Example Application
Exercises
-
Basic: Add NGRX DevTools to an existing Angular application and observe state changes as you interact with the app.
-
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.
-
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.
-
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! :)