Overview of NgRx
NgRx is a powerful state management library for Angular applications inspired by the Redux pattern. It enables developers to manage complex application states in a predictable, reactive, and centralized way.
At its core, NgRx uses Actions, Reducers, Selectors, and Effects to handle application logic and data flow. It leverages RxJS Observables to create reactive and unidirectional data streams that keep your application consistent and scalable.
NgRx helps solve a common Angular challenge — managing shared state across multiple components while keeping data synchronized and predictable.
Why Use NgRx?
As applications grow, managing state becomes challenging. Components need to share data, respond to user interactions, and update the UI efficiently.
NgRx provides a single source of truth for your application state, making it easier to:
- Predict and debug state changes.
- Share data between components easily.
- Handle complex asynchronous operations.
- Improve maintainability and scalability.
Core Concepts of NgRx
Before diving into code, let’s understand the core building blocks of NgRx:
Concept | Description |
---|---|
Store | Centralized state container that holds the application state. |
Action | An event that describes a change in the application. |
Reducer | A pure function that determines how the state changes based on an action. |
Selector | Functions used to retrieve specific pieces of state from the store. |
Effect | Handles side effects such as API calls and dispatches new actions based on responses. |
Installing NgRx
You can install NgRx packages using Angular CLI or npm.
Command:
ng add @ngrx/store
ng add @ngrx/effects
ng add @ngrx/store-devtools
Alternatively:
npm install @ngrx/store @ngrx/effects @ngrx/store-devtools
Setting Up the Store
First, configure the NgRx store in your application module.
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { StoreModule } from '@ngrx/store';
import { AppComponent } from './app.component';
import { counterReducer } from './state/counter.reducer';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
StoreModule.forRoot({ counter: counterReducer })
],
bootstrap: [AppComponent]
})
export class AppModule {}
This setup registers a global store with one feature slice — counter.
Defining Actions
Actions describe what happened in the application. They are plain objects containing a type
and optionally a payload
.
counter.actions.ts
import { createAction } from '@ngrx/store';
export const increment = createAction('[Counter] Increment');
export const decrement = createAction('[Counter] Decrement');
export const reset = createAction('[Counter] Reset');
Actions are dispatched to the store to trigger state changes.
Creating a Reducer
A reducer is a pure function that takes the current state and an action, then returns a new state.
counter.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { increment, decrement, reset } from './counter.actions';
export const initialState = 0;
export const counterReducer = createReducer(
initialState,
on(increment, state => state + 1),
on(decrement, state => state - 1),
on(reset, state => 0)
);
Reducers must be pure — they cannot modify the state directly, only return a new one.
Dispatching Actions from a Component
Actions are dispatched using the Store
service.
counter.component.ts
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { increment, decrement, reset } from './state/counter.actions';
@Component({
selector: 'app-counter',
template: `
<h1>Counter: {{ count$ | async }}</h1>
<button (click)="onIncrement()">Increment</button>
<button (click)="onDecrement()">Decrement</button>
<button (click)="onReset()">Reset</button>
`
})
export class CounterComponent {
count$ = this.store.select('counter');
constructor(private store: Store<{ counter: number }>) {}
onIncrement() {
this.store.dispatch(increment());
}
onDecrement() {
this.store.dispatch(decrement());
}
onReset() {
this.store.dispatch(reset());
}
}
This component connects directly to the NgRx store using selectors and dispatches actions based on user interaction.
Using Selectors
Selectors allow you to query specific pieces of state efficiently. They are reusable and improve performance by avoiding unnecessary recalculations.
counter.selectors.ts
import { createSelector, createFeatureSelector } from '@ngrx/store';
export const selectCounterState = createFeatureSelector<number>('counter');
export const selectCount = createSelector(
selectCounterState,
(state: number) => state
);
Using the Selector in a Component:
count$ = this.store.select(selectCount);
Selectors are especially useful in large applications with nested state structures.
Handling Side Effects with NgRx Effects
Effects handle asynchronous operations like API calls or logging. They listen for actions and can dispatch new actions based on the results.
counter.effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { tap } from 'rxjs/operators';
import { increment, decrement } from './counter.actions';
@Injectable()
export class CounterEffects {
logActions$ = createEffect(() =>
this.actions$.pipe(
ofType(increment, decrement),
tap(action => console.log('Action dispatched:', action))
),
{ dispatch: false }
);
constructor(private actions$: Actions) {}
}
Register Effects:
import { EffectsModule } from '@ngrx/effects';
import { CounterEffects } from './state/counter.effects';
@NgModule({
imports: [
EffectsModule.forRoot([CounterEffects])
]
})
export class AppModule {}
Effects make NgRx powerful for real-world apps where side effects are common.
Using NgRx DevTools
NgRx integrates with Redux DevTools, a Chrome extension for debugging state.
Setup:
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { environment } from '../environments/environment';
@NgModule({
imports: [
StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: environment.production })
]
})
export class AppModule {}
Now you can view state transitions, actions, and time-travel through your application’s history.
Feature Modules with NgRx
Large apps often need feature-level state management. You can configure a store for specific modules.
feature.module.ts
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { counterReducer } from './state/counter.reducer';
import { CounterEffects } from './state/counter.effects';
@NgModule({
imports: [
StoreModule.forFeature('counterFeature', counterReducer),
EffectsModule.forFeature([CounterEffects])
]
})
export class CounterFeatureModule {}
This approach keeps the application modular and scalable.
Managing Complex State
NgRx can handle deeply nested states using structured objects.
Example:
export interface AppState {
user: {
name: string;
email: string;
};
products: string[];
}
You can define separate reducers for each slice of state and combine them using StoreModule.forRoot()
.
Async Operations with NgRx Effects
Let’s see an example of fetching data from an API using Effects.
products.actions.ts
import { createAction, props } from '@ngrx/store';
export const loadProducts = createAction('[Products] Load');
export const loadProductsSuccess = createAction('[Products] Load Success', props<{ products: any[] }>());
export const loadProductsFailure = createAction('[Products] Load Failure', props<{ error: string }>());
products.effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { HttpClient } from '@angular/common/http';
import { mergeMap, map, catchError, of } from 'rxjs';
import { loadProducts, loadProductsSuccess, loadProductsFailure } from './products.actions';
@Injectable()
export class ProductsEffects {
loadProducts$ = createEffect(() =>
this.actions$.pipe(
ofType(loadProducts),
mergeMap(() =>
this.http.get<any[]>('/api/products').pipe(
map(products => loadProductsSuccess({ products })),
catchError(error => of(loadProductsFailure({ error: error.message })))
)
)
)
);
constructor(private actions$: Actions, private http: HttpClient) {}
}
products.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { loadProducts, loadProductsSuccess, loadProductsFailure } from './products.actions';
export interface ProductsState {
products: any[];
loading: boolean;
error: string | null;
}
export const initialState: ProductsState = {
products: [],
loading: false,
error: null
};
export const productsReducer = createReducer(
initialState,
on(loadProducts, state => ({ ...state, loading: true })),
on(loadProductsSuccess, (state, { products }) => ({ ...state, loading: false, products })),
on(loadProductsFailure, (state, { error }) => ({ ...state, loading: false, error }))
);
Using Selectors for Data Access
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { ProductsState } from './products.reducer';
export const selectProductsState = createFeatureSelector<ProductsState>('products');
export const selectAllProducts = createSelector(
selectProductsState,
(state) => state.products
);
export const selectLoading = createSelector(
selectProductsState,
(state) => state.loading
);
Now, components can use:
this.products$ = this.store.select(selectAllProducts);
Benefits of Using NgRx
- Predictable State Management – Every change is explicit and traceable.
- Centralized Data Flow – All data passes through a single source of truth.
- Improved Debugging – With DevTools, tracking actions and states is simple.
- Reactive Integration – Seamlessly works with RxJS for reactive applications.
- Testability – Each piece (actions, reducers, effects) can be unit tested.
Testing NgRx Components
NgRx makes testing easy because of its pure functions and predictable flow.
Testing a Reducer:
import { counterReducer, initialState } from './counter.reducer';
import { increment } from './counter.actions';
describe('Counter Reducer', () => {
it('should increment the counter', () => {
const newState = counterReducer(initialState, increment());
expect(newState).toBe(1);
});
});
Best Practices for NgRx
- Keep your state shape minimal and normalized.
- Use selectors for all state access.
- Avoid placing non-serializable data in the store.
- Split large stores into feature modules.
- Handle side effects only in Effects.
- Use createFeatureSelector and createSelector for reusability.
- Combine NgRx with EntityAdapter for managing collections efficiently.
Leave a Reply