Skip to content
intermediate Phase 12 · NgRx State Management

Reducers and Immutability

Produce the next state with pure reducer functions.

40m
0 problems
Topic Progress 0%

Creating Reducers

Reducers are pure functions that take the current state and an action, and return the new state. They must never mutate the existing state.

Creating a Reducer

import { createReducer, on } from '@ngrx/store';
import * as UserActions from './user.actions';

export interface UserState {
  users: User[];
  loading: boolean;
  error: string | null;
}

export const initialState: UserState = {
  users: [],
  loading: false,
  error: null,
};

export const userReducer = createReducer(
  initialState,

  on(UserActions.loadUsers, state => ({
    ...state,
    loading: true,
    error: null,
  })),

  on(UserActions.loadUsersSuccess, (state, { users }) => ({
    ...state,
    users,
    loading: false,
  })),

  on(UserActions.loadUsersFailure, (state, { error }) => ({
    ...state,
    loading: false,
    error,
  })),

  on(UserActions.addUser, (state, { user }) => ({
    ...state,
    users: [...state.users, user],
  })),

  on(UserActions.deleteUser, (state, { id }) => ({
    ...state,
    users: state.users.filter(u => u.id !== id),
  })),
);

Registering Reducers

import { provideStore } from '@ngrx/store';
import { userReducer } from './user.reducer';

export const appConfig: ApplicationConfig = {
  providers: [
    provideStore({ users: userReducer }),
  ],
};

Reducer Rules

  1. Pure functions — Same input always produces same output
  2. No side effects — No HTTP calls, no mutations
  3. Immutable updates — Always return new state objects
  4. Default case — Return current state for unknown actions
// BAD: Mutating state
state.users.push(action.user); // Mutation!
return state;

// GOOD: Immutable update
return {
  ...state,
  users: [...state.users, action.user],
};

Immutability Patterns

Immutability ensures change detection works correctly and enables time-travel debugging.

Immutable Update Patterns

// Array: add item
users: [...state.users, newItem]

// Array: remove item
users: state.users.filter(u => u.id !== id)

// Array: update item
users: state.users.map(u => u.id === id ? { ...u, ...changes } : u)

// Object: update property
loading: true

// Nested object: update deeply
...state,
address: {
  ...state.address,
  city: 'New City',
}

Returning the Same Reference

When nothing changes, return the same state reference. This lets selectors skip recalculation:

on(UserActions.someAction, state => {
  // If nothing changes, return the same reference
  if (state.users.length === 0) {
    return state; // Same reference — selectors skip
  }

  return {
    ...state,
    users: [...state.users, newItem],
  };
});

Using Immer for Complex Updates

import { produce } from 'immer';

export const userReducer = createReducer(
  initialState,
  on(UserActions.updateUser, (state, { id, changes }) =>
    produce(state, draft => {
      const user = draft.users.find(u => u.id === id);
      if (user) {
        Object.assign(user, changes);
      }
    })
  ),
);

Reducer Composition

// Combine multiple reducers
export const reducers = {
  users: userReducer,
  posts: postReducer,
  comments: commentReducer,
};

provideStore(reducers)

Quiz

1. What is a reducer in NgRx?

Question 1 options

2. Why must reducers be immutable?

Question 2 options

3. What should a reducer return for an unknown action?

Question 3 options

Flashcards

Question

What is a reducer?

Answer

Pure function (state, action) => newState. No side effects, immutable.

Question

Why immutability?

Answer

Change detection uses reference equality. Enables time-travel debugging.

Question

What to return for unknown actions?

Answer

The current state unchanged.

Revision Notes

Key Takeaways

  • 1. Reducers are pure functions
  • 2. Always return new state objects (immutable)
  • 3. Return same reference when nothing changes
  • 4. Use on() to handle actions

Interview Tips

  • • Explain why immutability matters
  • • Be ready to describe reducer composition
  • • Mention Immer for complex nested updates

Cheat Sheet

Reducers Cheat Sheet

Create reducer:

export const userReducer = createReducer(
  initialState,
  on(UserActions.loadUsers, state => ({ ...state, loading: true })),
  on(UserActions.loadUsersSuccess, (state, { users }) => ({
    ...state,
    users,
    loading: false,
  })),
);

Immutable updates:

// Add: [...state.items, newItem]
// Remove: state.items.filter(i => i.id !== id)
// Update: state.items.map(i => i.id === id ? { ...i, ...changes } : i)

Register:

provideStore({ users: userReducer })

Continue Learning

Before You Start

Related Reading