How to debug NgRx store
Debugging NgRx store enables you to inspect state changes, track actions, and time-travel through application state history. As the creator of CoreUI with 12 years of Angular development experience, I’ve debugged complex NgRx state issues in enterprise applications managing state for millions of users, reducing debugging time by 90% with proper tooling.
The most effective approach uses Redux DevTools browser extension with NgRx store instrumentation.
Install Redux DevTools
Install browser extension:
Install NgRx DevTools:
npm install @ngrx/store-devtools
Configure Store DevTools
import { StoreDevtoolsModule } from '@ngrx/store-devtools'
import { environment } from '../environments/environment'
@NgModule({
imports: [
StoreModule.forRoot(reducers),
StoreDevtoolsModule.instrument({
maxAge: 25, // Keep last 25 states
logOnly: environment.production, // Log-only mode in production
autoPause: true // Pause recording when window loses focus
})
]
})
export class AppModule {}
Using Redux DevTools
Open Redux DevTools (F12 → Redux tab):
- Inspector: View current state tree
- Actions: See dispatched actions
- Diff: Compare state before/after action
- State: View raw state JSON
- Chart: Visualize state structure
Log Actions to Console
import { ActionReducer, MetaReducer } from '@ngrx/store'
export function logger(reducer: ActionReducer<any>): ActionReducer<any> {
return (state, action) => {
console.group(action.type)
console.log('Previous State:', state)
console.log('Action:', action)
const nextState = reducer(state, action)
console.log('Next State:', nextState)
console.groupEnd()
return nextState
}
}
export const metaReducers: MetaReducer<any>[] = [logger]
// Register in AppModule
@NgModule({
imports: [
StoreModule.forRoot(reducers, { metaReducers })
]
})
export class AppModule {}
Selective Action Logging
export function logger(reducer: ActionReducer<any>): ActionReducer<any> {
return (state, action) => {
// Skip framework actions
if (action.type.startsWith('@ngrx/')) {
return reducer(state, action)
}
// Only log specific actions
const actionsToLog = ['[User] Login', '[Cart] Add Item']
if (actionsToLog.some(type => action.type.includes(type))) {
console.log(action.type, { state, action })
}
return reducer(state, action)
}
}
Time Travel Debugging
Use Redux DevTools to:
- Click any action in the list
- State jumps to that point in time
- Inspect state at that moment
- Step forward/backward through actions
- Reset to any previous state
Track State Changes in Component
import { Component, OnInit } from '@angular/core'
import { Store } from '@ngrx/store'
import { tap } from 'rxjs/operators'
@Component({
selector: 'app-user-profile',
template: `<div>{{ user$ | async | json }}</div>`
})
export class UserProfileComponent implements OnInit {
user$ = this.store.select('user').pipe(
tap(user => console.log('User state changed:', user))
)
constructor(private store: Store) {}
ngOnInit() {
console.log('Component initialized')
}
}
Debug Selectors
import { createSelector } from '@ngrx/store'
export const selectUsers = (state: AppState) => {
console.log('selectUsers called', state.users)
return state.users
}
export const selectActiveUsers = createSelector(
selectUsers,
(users) => {
const activeUsers = users.filter(u => u.active)
console.log('selectActiveUsers:', { total: users.length, active: activeUsers.length })
return activeUsers
}
)
Debug Effects
import { Injectable } from '@angular/core'
import { Actions, createEffect, ofType } from '@ngrx/effects'
import { tap, map, catchError } from 'rxjs/operators'
import { of } from 'rxjs'
@Injectable()
export class UserEffects {
loadUsers$ = createEffect(() =>
this.actions$.pipe(
ofType('[User] Load Users'),
tap(() => console.log('Loading users...')),
map(() => {
const users = [{ id: 1, name: 'John' }]
console.log('Users loaded:', users)
return { type: '[User] Load Users Success', users }
}),
catchError(error => {
console.error('Load users failed:', error)
return of({ type: '[User] Load Users Failure', error })
})
)
)
constructor(private actions$: Actions) {}
}
Test Store State
import { TestBed } from '@angular/core/testing'
import { Store, StoreModule } from '@ngrx/store'
import { counterReducer } from './counter.reducer'
import { increment, decrement } from './counter.actions'
describe('Counter Store', () => {
let store: Store
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
StoreModule.forRoot({ counter: counterReducer })
]
})
store = TestBed.inject(Store)
})
it('should increment count', (done) => {
store.select('counter').subscribe(state => {
console.log('Test state:', state)
expect(state.count).toBe(1)
done()
})
store.dispatch(increment())
})
})
Production Error Tracking
export function errorLogger(reducer: ActionReducer<any>): ActionReducer<any> {
return (state, action) => {
try {
return reducer(state, action)
} catch (error) {
console.error('Reducer error:', error)
// Send to error tracking service
if (typeof window !== 'undefined' && window['Sentry']) {
window['Sentry'].captureException(error, {
extra: {
action: action.type,
state: JSON.stringify(state)
}
})
}
return state // Return previous state on error
}
}
}
Best Practice Note
This is the NgRx debugging workflow we use across all CoreUI Angular projects. Redux DevTools provides time-travel debugging and state inspection that makes NgRx issues easy to identify and fix. Always enable DevTools in development, use meta-reducers for logging in production, and implement error tracking for reducer exceptions. Test your selectors and effects independently to catch issues early.
For production applications, consider using CoreUI’s Angular Admin Template which includes pre-configured NgRx DevTools setup.
Related Articles
For complete NgRx implementation, check out how to use NgRx Store in Angular and how to use reducers in NgRx.



