NgRx data flow in Angular follows a predictable loop: component dispatches actions, reducers create the next immutable state, selectors read and derive view data, and effects handle async side effects before dispatching success/failure actions. This is the core actions-reducers-effects-selectors flow interviewers expect.
NgRx data flow end-to-end in Angular: actions, reducers, effects, selectors
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
Core mental model
Think of NgRx as a one-way loop:Component event -> dispatch(action) -> reducer computes new state -> selector derives VM -> template renders
Effects run beside this loop for async work (HTTP, analytics, router side effects), then dispatch follow-up actions like success/failure.
Step | Who does it | What happens | Why it matters |
|---|---|---|---|
| Component | User clicks/searches; component dispatches an action | Events become explicit and traceable |
| Reducer | Pure function returns next immutable state | Predictable updates and easy debugging |
| Effect | Listens to action, calls API, dispatches success/failure | Keeps reducers pure and components thin |
| Selector | Memoized selection/derivation from store state | Performance + reusable view logic |
| Template + async pipe | Subscribes to selector output and updates UI | Reactive view with minimal manual subscriptions |
// books.actions.ts
import { createAction, props } from '@ngrx/store';
export const loadBooks = createAction('[Books Page] Load Books');
export const loadBooksSuccess = createAction(
'[Books API] Load Books Success',
props<{ books: Book[] }>()
);
export const loadBooksFailure = createAction(
'[Books API] Load Books Failure',
props<{ error: string }>()
);
// books.reducer.ts
import { createReducer, on } from '@ngrx/store';
export interface BooksState {
books: Book[];
loading: boolean;
error: string | null;
}
export const initialState: BooksState = {
books: [],
loading: false,
error: null
};
export const booksReducer = createReducer(
initialState,
on(loadBooks, state => ({ ...state, loading: true, error: null })),
on(loadBooksSuccess, (state, { books }) => ({ ...state, books, loading: false })),
on(loadBooksFailure, (state, { error }) => ({ ...state, error, loading: false }))
);
// books.effects.ts
import { Injectable, inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { catchError, map, of, switchMap } from 'rxjs';
@Injectable()
export class BooksEffects {
private actions$ = inject(Actions);
private api = inject(BooksApiService);
loadBooks$ = createEffect(() =>
this.actions$.pipe(
ofType(loadBooks),
switchMap(() =>
this.api.getBooks().pipe(
map(books => loadBooksSuccess({ books })),
catchError(err => of(loadBooksFailure({ error: String(err) })))
)
)
)
);
}
// books.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
export const selectBooksState = createFeatureSelector<BooksState>('books');
export const selectBooks = createSelector(selectBooksState, s => s.books);
export const selectLoading = createSelector(selectBooksState, s => s.loading);
export const selectError = createSelector(selectBooksState, s => s.error);
export const selectVm = createSelector(
selectBooks,
selectLoading,
selectError,
(books, loading, error) => ({ books, loading, error, total: books.length })
);
// books-page.component.ts
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { Store } from '@ngrx/store';
@Component({
selector: 'app-books-page',
template: `
<button (click)="reload()">Reload</button>
<ng-container *ngIf="vm$ | async as vm">
<p *ngIf="vm.loading">Loading...</p>
<p *ngIf="vm.error">Error: {{ vm.error }}</p>
<p>Total: {{ vm.total }}</p>
<li *ngFor="let b of vm.books">{{ b.title }}</li>
</ng-container>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BooksPageComponent {
private store = inject(Store);
readonly vm$ = this.store.select(selectVm);
ngOnInit(): void {
this.store.dispatch(loadBooks());
}
reload(): void {
this.store.dispatch(loadBooks());
}
}
Common mistake | Why it breaks | Fix |
|---|---|---|
Putting HTTP in reducers | Reducers must be synchronous and pure | Move async work to effects |
Doing heavy mapping in components | Duplicates logic and hurts performance | Use memoized selectors for derived view models |
Subscribing manually everywhere | Leak risk and boilerplate | Use |
Dispatching vague action names | Hard to trace intent in DevTools | Use event-style action naming ( |
Interview summary
In Angular state management with NgRx, components dispatch actions for user intent, reducers compute next immutable state, selectors expose memoized read models, and templates render selector output. Effects handle async side effects and dispatch success/failure actions back into the same loop. If you can explain that loop clearly, you understand NgRx data flow.