NgRx data flow is a predictable loop, but the high-value explanation is operational: UI intent dispatches actions, reducers stay pure, effects handle async side effects, and selectors expose debug-friendly read models back to Angular components.
Use this Angular interview question to rehearse a quick answer, common mistake, follow-up, and production pitfall.
NgRx data flow end-to-end in Angular: actions, reducers, effects, selectorsFrontend interview answer
This Angular interview question tests whether you can explain NgRx data flow in Angular: actions, reducers, effects, selectors, and debug loops, connect it to production trade-offs, and handle common follow-up questions.
- NgRx data flow in Angular: actions, reducers, effects, selectors, and debug loops explanation without falling back to memorized docs wording
- Store and Selectors reasoning, edge cases, and production failure modes
- How you would answer the most likely Angular interview follow-up
Operational loop
NgRx is most useful when you explain it as a debuggable one-way loop: the UI dispatches intent, reducers compute immutable state, effects run async side effects beside that loop, and selectors expose a clean read model back to the template. That framing is what separates “I know the words” from production understanding.
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 ( |
Pure reducer update vs effect-driven async update
A quick way to explain NgRx clearly is to separate the sync transition from the async side effect. Reducers update flags synchronously. Effects handle API work and feed the result back into the store.
// reducer: synchronous state transition
on(loadBooks, state => ({ ...state, loading: true, error: null }))
// effect: async work beside the reducer loop
loadBooks$ = createEffect(() =>
this.actions$.pipe(
ofType(loadBooks),
switchMap(() => this.api.getBooks().pipe(
map((books) => loadBooksSuccess({ books })),
catchError((error) => of(loadBooksFailure({ error: String(error) })))
))
)
);
Compact trace you should be able to say out loudLoad Books click → loadBooks action → reducer sets loading=true → effect calls API → loadBooksSuccess/loadBooksFailure → reducer stores result → selector builds vm → template re-renders. That is the end-to-end loop interviewers want.
Selectors are memoized read models
A selector is not just a getter. It is the read layer that turns raw store state into a reusable, memoized view model so components do not keep sorting, filtering, and combining the same data in multiple places.
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.
Use this as one explanation rep, then continue with the Angular interview questions cluster or a guided prep path.