NgRx reducer pure function in practice: immutability, side effects, and common mistakes

HighIntermediateAngular
Preparing for interviews?

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

Quick Answer

A reducer in NgRx must be a pure function: same input state + action must always produce the same next state, with no side effects. Interviewers test reducer immutability and whether you can spot forbidden reducer side effects like mutation, Math.random(), HTTP, router navigation, and service calls.

Answer

Core idea

An NgRx reducer is a synchronous, deterministic state transition function. It should only compute the next immutable state from (state, action). If it reaches outside (network, router, random values, services), it is no longer a pure reducer.

Forbidden in reducers

Why it is wrong

Where it belongs

State mutation (push, direct assignment, in-place edits)

Breaks immutability and memoization assumptions

Return new objects/arrays in reducer

Math.random() / time-dependent values

Same input may produce different output

Generate IDs/timestamps before dispatch or in effects

HTTP calls

Async side effect inside supposedly pure function

Effects

Router navigation

External side effect not a state calculation

Effects or component/facade orchestration

Service calls (analytics, storage, logging APIs)

Leaks imperative side effects into reducer

Effects or dedicated services triggered elsewhere

Reducer purity guardrails
TYPESCRIPT
// ❌ Bad reducer: impure + mutating
export const ordersReducer = createReducer(
  initialState,
  on(placeOrder, (state, { item, router, api, audit }) => {
    state.orders.push(item); // mutation

    const id = Math.random().toString(36).slice(2); // non-deterministic

    api.postOrder(item).subscribe(); // HTTP side effect
    router.navigate(['/orders', id]); // navigation side effect
    audit.track('order_placed', item); // service side effect

    return state; // same reference returned
  })
);
                  
TYPESCRIPT
// ✅ Good reducer: pure + immutable
export interface OrdersState {
  orders: Array<{ id: string; name: string }>;
  loading: boolean;
  error: string | null;
}

export const initialState: OrdersState = {
  orders: [],
  loading: false,
  error: null
};

export const ordersReducer = createReducer(
  initialState,
  on(placeOrder, state => ({ ...state, loading: true, error: null })),
  on(placeOrderSuccess, (state, { order }) => ({
    ...state,
    orders: [...state.orders, order],
    loading: false
  })),
  on(placeOrderFailure, (state, { error }) => ({ ...state, error, loading: false }))
);
                  
TYPESCRIPT
// ✅ Side effects move to effects
@Injectable()
export class OrdersEffects {
  placeOrder$ = createEffect(() =>
    this.actions$.pipe(
      ofType(placeOrder),
      switchMap(({ draft }) =>
        this.api.postOrder(draft).pipe(
          map(order => placeOrderSuccess({ order })),
          catchError(err => of(placeOrderFailure({ error: String(err) })))
        )
      )
    )
  );

  navigateOnSuccess$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(placeOrderSuccess),
        tap(({ order }) => this.router.navigate(['/orders', order.id]))
      ),
    { dispatch: false }
  );

  constructor(
    private actions$: Actions,
    private api: OrdersApiService,
    private router: Router
  ) {}
}
                  

Reducer purity checklist

Pass condition

Deterministic output

Same state + action always yields same next state

No mutation

Original state untouched; new references returned

No async work

No HTTP/subscription/promise in reducer

No framework side effects

No router navigation, no service invocations

Synchronous and small

Just state transition logic, nothing else

Quick review rubric before merge

Interview summary

An NgRx reducer pure function does one thing: compute next immutable state. It must avoid mutation and all side effects, including Math.random(), HTTP, router calls, and service usage. Put side effects in effects, keep reducers deterministic, and selector memoization and debugging stay reliable.

Similar questions
Guides
4 / 43