NgRx data flow end-to-end in Angular: actions, reducers, effects, selectors

HighIntermediateAngular
Preparing for interviews?

Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.

Quick Answer

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.

Answer

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

    • UI intent

Component

User clicks/searches; component dispatches an action

Events become explicit and traceable

    • State transition

Reducer

Pure function returns next immutable state

Predictable updates and easy debugging

    • Async side effect

Effect

Listens to action, calls API, dispatches success/failure

Keeps reducers pure and components thin

    • Read model

Selector

Memoized selection/derivation from store state

Performance + reusable view logic

    • Render

Template + async pipe

Subscribes to selector output and updates UI

Reactive view with minimal manual subscriptions

NgRx data flow diagram (end-to-end loop)
TYPESCRIPT
// 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 store.select(...) + async pipe

Dispatching vague action names

Hard to trace intent in DevTools

Use event-style action naming ([Source] Event)

What interviewers flag quickly

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.

Similar questions
Guides
3 / 43