An NgRx reducer must be deterministic, immutable, and side-effect free. The same state and action should always produce the same next state, while HTTP, router work, services, random values, and timestamps belong in effects instead.
Use this Angular interview question to rehearse a quick answer, common mistake, follow-up, and production pitfall.
NgRx reducer pure function in practice: immutability, side effects, and common mistakesFrontend interview answer
This Angular interview question tests whether you can explain Pure reducers in NgRx: immutability, forbidden side effects, and effects boundaries, connect it to production trade-offs, and handle common follow-up questions.
- Pure reducers in NgRx: immutability, forbidden side effects, and effects boundaries explanation without falling back to memorized docs wording
- Store and State Management reasoning, edge cases, and production failure modes
- How you would answer the most likely Angular interview follow-up
Deterministic reducer rule
An NgRx reducer is a synchronous, deterministic state transition function. The same input (state, action) must always produce the same next state, and it must do so without mutation or side effects. If it reaches outside to the network, router, services, random values, or timestamps, it is no longer a pure reducer.
Worked exampleaddTodo should return a new array based only on the previous state and the dispatched payload. It must not call Math.random(), fetch data, navigate, or ask a service for extra information. Those side effects belong in effects, not in the reducer.
Boundary rule
- Reducers compute the next immutable state.
- Effects handle HTTP, router work, timers, and other side effects.
- Pure reducers keep state serializable, predictable, and testable.
Forbidden in reducers | Why it is wrong | Where it belongs |
|---|---|---|
State mutation ( | Breaks immutability and memoization assumptions | Return new objects/arrays in reducer |
| 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 |
// ❌ 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
})
);
// ✅ 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 }))
);
// ✅ 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 |
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.
Use this as one explanation rep, then continue with the Angular interview questions cluster or a guided prep path.