Loading...
Loading...
Debug and visualize NgRx state management in Angular apps with real-time action monitoring, effect tracking, state diffs, and performance metrics
npx skill4agent add aradotso/devtools-skills ngrx-devtool-debuggerSkill by ara.so — Devtools Skills collection.
npm install --save-dev @amadeus-it-group/ngrx-devtoolyarn add -D @amadeus-it-group/ngrx-devtool// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideStore } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects';
import {
provideNgrxDevTool,
createDevToolMetaReducer
} from '@amadeus-it-group/ngrx-devtool';
import * as fromRoot from './store/reducers';
import { AppEffects } from './store/effects/app.effects';
export const appConfig: ApplicationConfig = {
providers: [
provideStore(
fromRoot.reducers,
{
metaReducers: [createDevToolMetaReducer()]
}
),
provideEffects([AppEffects]),
provideNgrxDevTool({
wsUrl: 'ws://localhost:4000',
trackEffects: true,
enabled: true
})
]
};// app.module.ts
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import {
NgRxDevToolModule,
createDevToolMetaReducer
} from '@amadeus-it-group/ngrx-devtool';
import { reducers } from './store/reducers';
import { AppEffects } from './store/effects/app.effects';
@NgModule({
imports: [
StoreModule.forRoot(reducers, {
metaReducers: [createDevToolMetaReducer()]
}),
EffectsModule.forRoot([AppEffects]),
NgRxDevToolModule.forRoot({
wsUrl: 'ws://localhost:4000',
trackEffects: true
})
]
})
export class AppModule {}npx ngrx-devtoolws://localhost:4000http://localhost:3000# Change WebSocket port
npx ngrx-devtool --ws-port 5000
# Change UI port
npx ngrx-devtool --ui-port 8080
# Custom ports for both
npx ngrx-devtool --ws-port 5000 --ui-port 8080npx ngrx-devtool --server-onlyinterface NgRxDevToolConfig {
// WebSocket server URL (default: 'ws://localhost:4000')
wsUrl?: string;
// Enable/disable the devtool (default: true in development)
enabled?: boolean;
// Track NgRx effects (default: true)
trackEffects?: boolean;
// Maximum number of actions to store (default: 100)
maxActions?: number;
// Automatically connect on initialization (default: true)
autoConnect?: boolean;
// Reconnection attempts (default: 3)
reconnectAttempts?: number;
// Reconnection delay in ms (default: 1000)
reconnectDelay?: number;
}// app.config.ts
import { isDevMode } from '@angular/core';
import { provideNgrxDevTool, createDevToolMetaReducer } from '@amadeus-it-group/ngrx-devtool';
export const appConfig: ApplicationConfig = {
providers: [
provideStore(
reducers,
{
metaReducers: isDevMode() ? [createDevToolMetaReducer()] : []
}
),
provideNgrxDevTool({
wsUrl: `ws://${window.location.hostname}:4000`,
enabled: isDevMode(),
trackEffects: true,
maxActions: 200,
reconnectAttempts: 5
})
]
};// app.config.ts
import { environment } from './environments/environment';
export const appConfig: ApplicationConfig = {
providers: [
provideStore(
reducers,
{
metaReducers: environment.devToolEnabled
? [createDevToolMetaReducer()]
: []
}
),
provideNgrxDevTool({
wsUrl: environment.devToolWsUrl,
enabled: environment.devToolEnabled,
trackEffects: environment.devToolEnabled
})
]
};// environments/environment.ts
export const environment = {
production: false,
devToolEnabled: true,
devToolWsUrl: 'ws://localhost:4000'
};
// environments/environment.prod.ts
export const environment = {
production: true,
devToolEnabled: false,
devToolWsUrl: ''
};// user.actions.ts
import { createAction, props } from '@ngrx/store';
export const loadUsers = createAction('[User List] Load Users');
export const loadUsersSuccess = createAction(
'[User API] Load Users Success',
props<{ users: User[] }>()
);
export const loadUsersFailure = createAction(
'[User API] Load Users Failure',
props<{ error: string }>()
);
// Component dispatch - automatically tracked
this.store.dispatch(loadUsers());trackEffects: true// user.effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { catchError, map, switchMap } from 'rxjs/operators';
import { of } from 'rxjs';
import * as UserActions from './user.actions';
import { UserService } from '../services/user.service';
@Injectable()
export class UserEffects {
// This effect will be tracked by NgRx DevTool
loadUsers$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.loadUsers),
switchMap(() =>
this.userService.getUsers().pipe(
map(users => UserActions.loadUsersSuccess({ users })),
catchError(error => of(UserActions.loadUsersFailure({
error: error.message
})))
)
)
)
);
constructor(
private actions$: Actions,
private userService: UserService
) {}
}// counter.reducer.ts
import { createReducer, on } from '@ngrx/store';
import * as CounterActions from './counter.actions';
export interface CounterState {
count: number;
lastUpdated: Date | null;
}
export const initialState: CounterState = {
count: 0,
lastUpdated: null
};
export const counterReducer = createReducer(
initialState,
on(CounterActions.increment, state => ({
...state,
count: state.count + 1,
lastUpdated: new Date()
})),
on(CounterActions.decrement, state => ({
...state,
count: state.count - 1,
lastUpdated: new Date()
})),
on(CounterActions.reset, () => initialState)
);// product.actions.ts
import { createAction, props } from '@ngrx/store';
export const addToCart = createAction(
'[Product] Add to Cart',
props<{
productId: string;
quantity: number;
metadata?: { source: string; timestamp: number }
}>()
);
// Dispatch with metadata
this.store.dispatch(addToCart({
productId: 'prod-123',
quantity: 2,
metadata: {
source: 'product-detail-page',
timestamp: Date.now()
}
}));// app.config.ts
import { ActionReducer, Action } from '@ngrx/store';
import { createDevToolMetaReducer } from '@amadeus-it-group/ngrx-devtool';
function createFilteredDevToolMetaReducer() {
const devToolMetaReducer = createDevToolMetaReducer();
return (reducer: ActionReducer<any>) => {
const wrappedReducer = devToolMetaReducer(reducer);
return (state: any, action: Action) => {
// Skip tracking for high-frequency actions
if (action.type.includes('[Router]') ||
action.type.includes('[Internal]')) {
return reducer(state, action);
}
return wrappedReducer(state, action);
};
};
}
export const appConfig: ApplicationConfig = {
providers: [
provideStore(reducers, {
metaReducers: [createFilteredDevToolMetaReducer()]
}),
provideNgrxDevTool()
]
};// app.config.ts
const devToolConfig = (() => {
const hostname = window.location.hostname;
if (hostname === 'localhost') {
return { wsUrl: 'ws://localhost:4000', enabled: true };
} else if (hostname.includes('staging')) {
return { wsUrl: 'ws://staging-devtool.example.com:4000', enabled: true };
}
return { enabled: false };
})();
export const appConfig: ApplicationConfig = {
providers: [
provideNgrxDevTool(devToolConfig)
]
};npx ngrx-devtoolprovideNgrxDevTool({
wsUrl: 'ws://localhost:4000' // Must match server port
})WebSocket connection to 'ws://localhost:4000' failed: Connection refusedprovideStore(reducers, {
metaReducers: [createDevToolMetaReducer()] // Must be present
})provideNgrxDevTool({
enabled: true // Check this is true
})provideNgrxDevTool({
trackEffects: true // Must be true
})provideEffects([UserEffects, ProductEffects])createEffect()provideNgrxDevTool({
maxActions: 50 // Default is 100
})// Use custom meta-reducer to skip router/polling actionsprovideNgrxDevTool({
enabled: !environment.production
})npx ngrx-devtool --ws-port 5000 --ui-port 8080provideNgrxDevTool({
wsUrl: 'ws://localhost:5000'
})// ❌ Bad - mutates state
on(updateUser, (state, { user }) => {
state.user = user; // Don't do this
return state;
})
// ✅ Good - returns new state
on(updateUser, (state, { user }) => ({
...state,
user
}))