Angular State Management with NgRx Explained — Complete Guide
In this tutorial, you will learn about Angular State Management with NgRx Explained. We cover key concepts, practical examples, and best practices to help you master this topic.
NgRx is a state management library for Angular based on the Redux pattern, providing a predictable state container with unidirectional data flow for complex applications.
What You'll Learn
- The Redux pattern and why it matters for Angular
- How to set up NgRx Store, Actions, and Reducers
- How to use Selectors to query state efficiently
- How to handle side effects with NgRx Effects
- How to use NgRx Entity for normalized data
Why It Matters
As Angular apps grow, managing shared state becomes complex. NgRx provides a single source of truth, predictable state changes, and powerful developer tools for debugging. It is the standard for enterprise Angular applications.
Real-World Use
Durga Antivirus Pro uses NgRx to manage scan state, threat data, user preferences, and notification state across dozens of components. The NgRx DevTools allow developers to replay state changes during debugging.
flowchart LR
A[Component] -->|dispatch Action| B[Store]
B -->|Reducer| C[New State]
C -->|Selector| A
B -->|Action Stream| D[Effect]
D -->|API Call| E[External Service]
E -->|New Action| B
style A fill:#f97316,color:#fff
Core Concepts
NgRx follows the Redux pattern with three core principles:
- Single source of truth — The application state is stored in a single object tree within the Store.
- State is read-only — State can only be changed by dispatching Actions.
- Changes are made with pure functions — Reducers are pure functions that take the current state and an action, and return a new state.
Setting Up NgRx
Install NgRx packages:
npm install @ngrx/store @ngrx/effects @ngrx/entity @ngrx/store-devtools
Configure the store in the app:
import { bootstrapApplication } from "@angular/platform-browser";
import { provideState, provideStore } from "@ngrx/store";
import { provideEffects } from "@ngrx/effects";
import { provideStoreDevtools } from "@ngrx/store-devtools";
import { AppComponent } from "./app/app.component";
import { taskReducer } from "./store/task.reducer";
import { TaskEffects } from "./store/task.effects";
bootstrapApplication(AppComponent, {
providers: [
provideStore(),
provideState({ name: "tasks", reducer: taskReducer }),
provideEffects([TaskEffects]),
provideStoreDevtools({ maxAge: 25 })
]
});
Expected output: NgRx is initialized with a task feature state. The DevTools extension enables time-travel debugging.
provideStore() creates the root store. provideState registers feature states. Each feature state is a slice of the global state tree.
Actions
Actions describe state changes as events:
import { createAction, props } from "@ngrx/store";
export interface Task {
id: string;
title: string;
completed: boolean;
}
// Action types
export const loadTasks = createAction("[Task List] Load Tasks");
export const loadTasksSuccess = createAction(
"[Task API] Load Tasks Success",
props<{ tasks: Task[] }>()
);
export const loadTasksFailure = createAction(
"[Task API] Load Tasks Failure",
props<{ error: string }>()
);
export const addTask = createAction(
"[Task List] Add Task",
props<{ title: string }>()
);
export const addTaskSuccess = createAction(
"[Task API] Add Task Success",
props<{ task: Task }>()
);
export const toggleTask = createAction(
"[Task List] Toggle Task",
props<{ id: string }>()
);
export const removeTask = createAction(
"[Task List] Remove Task",
props<{ id: string }>()
);
Expected output: Strongly-typed action creators with descriptive type strings.
Action types follow the convention [Source] Event. This makes it clear where the action originates and what it represents. Each action is a function that returns an action object with type and props properties.
Reducers
Reducers are pure functions that transform state:
import { createReducer, on } from "@ngrx/store";
import * as TaskActions from "./task.actions";
import { Task } from "./task.actions";
export interface TaskState {
tasks: Task[];
loading: boolean;
error: string | null;
}
export const initialState: TaskState = {
tasks: [],
loading: false,
error: null
};
export const taskReducer = createReducer(
initialState,
on(TaskActions.loadTasks, state => ({
...state,
loading: true,
error: null
})),
on(TaskActions.loadTasksSuccess, (state, { tasks }) => ({
...state,
tasks,
loading: false
})),
on(TaskActions.loadTasksFailure, (state, { error }) => ({
...state,
error,
loading: false
})),
on(TaskActions.addTaskSuccess, (state, { task }) => ({
...state,
tasks: [...state.tasks, task]
})),
on(TaskActions.toggleTask, (state, { id }) => ({
...state,
tasks: state.tasks.map(task =>
task.id === id ? { ...task, completed: !task.completed } : task
)
})),
on(TaskActions.removeTask, (state, { id }) => ({
...state,
tasks: state.tasks.filter(task => task.id !== id)
}))
);
Expected output: State transitions are explicit and predictable. Each action handler returns a new state object with immutable updates.
Reducers must be pure: no side effects, no API calls, no random values. The spread operator (...state) ensures immutability. Each handler returns a brand new state reference.
Selectors
Selectors efficiently derive data from the store:
import { createSelector, createFeatureSelector } from "@ngrx/store";
import { TaskState } from "./task.reducer";
import { Task } from "./task.actions";
// Feature selector
export const selectTaskState = createFeatureSelector<TaskState>("tasks");
// Base selectors
export const selectAllTasks = createSelector(
selectTaskState,
state => state.tasks
);
export const selectLoading = createSelector(
selectTaskState,
state => state.loading
);
export const selectError = createSelector(
selectTaskState,
state => state.error
);
// Derived selectors
export const selectCompletedTasks = createSelector(
selectAllTasks,
tasks => tasks.filter(task => task.completed)
);
export const selectPendingTasks = createSelector(
selectAllTasks,
tasks => tasks.filter(task => !task.completed)
);
export const selectTaskCount = createSelector(
selectAllTasks,
tasks => tasks.length
);
export const selectTaskById = (id: string) => createSelector(
selectAllTasks,
tasks => tasks.find(task => task.id === id)
);
Expected output: Selectors compute derived state efficiently with memoization. The same selector with the same arguments returns the cached result.
Selectors compose. createFeatureSelector selects the feature slice. createSelector combines selectors to compute derived values. Selectors are re-evaluated only when their input state changes.
Effects
Effects handle side effects like API calls:
import { Injectable, inject } from "@angular/core";
import { Actions, createEffect, ofType } from "@ngrx/effects";
import { HttpClient } from "@angular/common/http";
import { of } from "rxjs";
import { catchError, map, mergeMap, switchMap } from "rxjs/operators";
import * as TaskActions from "./task.actions";
import { Task } from "./task.actions";
@Injectable()
export class TaskEffects {
private actions$ = inject(Actions);
private http = inject(HttpClient);
loadTasks$ = createEffect(() =>
this.actions$.pipe(
ofType(TaskActions.loadTasks),
switchMap(() =>
this.http.get<Task[]>("https://jsonplaceholder.typicode.com/todos?_limit=10").pipe(
map(tasks => TaskActions.loadTasksSuccess({ tasks })),
catchError(error => of(TaskActions.loadTasksFailure({ error: error.message })))
)
)
)
);
addTask$ = createEffect(() =>
this.actions$.pipe(
ofType(TaskActions.addTask),
switchMap(action =>
this.http.post<Task>("https://jsonplaceholder.typicode.com/todos", {
title: action.title,
completed: false
}).pipe(
map(task => TaskActions.addTaskSuccess({ task })),
catchError(error => of(TaskActions.loadTasksFailure({ error: error.message })))
)
)
)
);
}
Expected output: When loadTasks dispatches, the effect makes an HTTP call and dispatches success or failure.
Effects listen for specific actions using ofType, perform side effects, and dispatch new actions with the results. They return an Observable of actions that the store dispatches.
Using Store in Components
Components interact with the store through selectors and dispatch:
import { Component, OnInit } from "@angular/core";
import { Store } from "@ngrx/store";
import { Observable } from "rxjs";
import { CommonModule } from "@angular/common";
import * as TaskActions from "./store/task.actions";
import { selectAllTasks, selectLoading, selectTaskCount } from "./store/task.selectors";
import { Task } from "./store/task.actions";
@Component({
selector: "app-task-list",
standalone: true,
imports: [CommonModule],
template: `
<div *ngIf="loading$ | async">Loading tasks...</div>
<p>Total tasks: {{ taskCount$ | async }}</p>
<ul>
<li *ngFor="let task of tasks$ | async">
<input type="checkbox" [checked]="task.completed"
(change)="toggleTask(task.id)" />
{{ task.title }}
</li>
</ul>
<button (click)="addSampleTask()">Add Sample Task</button>
`
})
export class TaskListComponent implements OnInit {
tasks$: Observable<Task[]>;
loading$: Observable<boolean>;
taskCount$: Observable<number>;
constructor(private store: Store) {
this.tasks$ = this.store.select(selectAllTasks);
this.loading$ = this.store.select(selectLoading);
this.taskCount$ = this.store.select(selectTaskCount);
}
ngOnInit() {
this.store.dispatch(TaskActions.loadTasks());
}
toggleTask(id: string) {
this.store.dispatch(TaskActions.toggleTask({ id }));
}
addSampleTask() {
this.store.dispatch(TaskActions.addTask({ title: "New Task " + Date.now() }));
}
}
Expected output: The component displays tasks from the store, dispatches actions on user interaction, and shows a loading indicator while fetching.
Components use the async pipe to subscribe to selectors. They never modify state directly — they dispatch actions that go through reducers to produce new state.
Common Mistakes
Putting non-serializable data in the store — The store should only contain serializable data. Functions, class instances, and DOM elements do not belong in the store.
Side effects in reducers — Reducers must be pure. API calls, localStorage access, or random value generation belong in Effects, not reducers.
Over-normalizing state — Not everything belongs in NgRx. Local form state, UI toggles, and temporary data can stay in components.
Selectors that return new references each time — Selectors should memoize. If a selector returns a new array each call, it triggers unnecessary change detection.
Not using Entity for collections — NgRx Entity provides optimized reducer functions for CRUD operations on collections. Use it instead of manual array updates.
Practice Questions
What are the three core principles of NgRx? Single source of truth, state is read-only, changes are made with pure functions (reducers).
What is the purpose of an Effect? To handle side effects like API calls, timers, and external interactions by dispatching actions with results.
What is a Selector? A pure function that derives data from the store state with memoization for performance.
How do components read state from the store? Using the
store.select(selector)method or the async pipe with selectors.What is NgRx Entity? A library that provides normalized state management for collections with optimized reducer functions.
Challenge
Build a NgRx store for a ShoppingCartModule. Create actions for addItem, removeItem, updateQuantity, applyCoupon, clearCart. Create reducers with NgRx Entity for cart items. Create selectors for itemCount, subtotal, tax, discount, and total. Create an effect that saves the cart to localStorage on every change and loads it on init.
FAQ
Mini Project
Build a TaskBoardApp with full NgRx state management. Create actions for CRUD operations on tasks and task lists. Use NgRx Entity for tasks within each list. Create effects that simulate API calls (use of with delay). Create selectors for task counts per list, overdue tasks, and completed tasks. Build components: BoardComponent, ListComponent, TaskCardComponent. Use the DevTools to verify action flow. Add a feature for dragging tasks between lists with appropriate actions.
What's Next
Complete the Angular learning path with the final project:
Angular Project, Angular Signals, Angular Standalone
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro