How to use reducers in NgRx

NgRx reducers are pure functions that handle state transitions based on dispatched actions, ensuring predictable and testable state management. As the creator of CoreUI with 12 years of Angular development experience, I’ve implemented NgRx reducers in enterprise Angular applications managing complex state for millions of users with zero state-related bugs.

The most maintainable approach uses createReducer with on handlers for type-safe action handling.

Basic Reducer

Create src/app/store/counter.reducer.ts:

import { createReducer, on } from '@ngrx/store'
import { increment, decrement, reset } from './counter.actions'

export interface CounterState {
  count: number
}

export const initialState: CounterState = {
  count: 0
}

export const counterReducer = createReducer(
  initialState,
  on(increment, (state) => ({
    ...state,
    count: state.count + 1
  })),
  on(decrement, (state) => ({
    ...state,
    count: state.count - 1
  })),
  on(reset, (state) => ({
    ...state,
    count: 0
  }))
)

Reducer with Payload

import { createReducer, on } from '@ngrx/store'
import { addTodo, toggleTodo, removeTodo, updateTodo } from './todo.actions'

export interface Todo {
  id: string
  title: string
  completed: boolean
}

export interface TodoState {
  todos: Todo[]
}

export const initialState: TodoState = {
  todos: []
}

export const todoReducer = createReducer(
  initialState,
  on(addTodo, (state, { title }) => ({
    ...state,
    todos: [
      ...state.todos,
      {
        id: Date.now().toString(),
        title,
        completed: false
      }
    ]
  })),
  on(toggleTodo, (state, { id }) => ({
    ...state,
    todos: state.todos.map(todo =>
      todo.id === id
        ? { ...todo, completed: !todo.completed }
        : todo
    )
  })),
  on(removeTodo, (state, { id }) => ({
    ...state,
    todos: state.todos.filter(todo => todo.id !== id)
  })),
  on(updateTodo, (state, { id, title }) => ({
    ...state,
    todos: state.todos.map(todo =>
      todo.id === id
        ? { ...todo, title }
        : todo
    )
  }))
)

Actions Definition

Create src/app/store/todo.actions.ts:

import { createAction, props } from '@ngrx/store'

export const addTodo = createAction(
  '[Todo] Add Todo',
  props<{ title: string }>()
)

export const toggleTodo = createAction(
  '[Todo] Toggle Todo',
  props<{ id: string }>()
)

export const removeTodo = createAction(
  '[Todo] Remove Todo',
  props<{ id: string }>()
)

export const updateTodo = createAction(
  '[Todo] Update Todo',
  props<{ id: string; title: string }>()
)

Complex Reducer with Loading States

import { createReducer, on } from '@ngrx/store'
import {
  loadUsers,
  loadUsersSuccess,
  loadUsersFailure
} from './user.actions'

export interface User {
  id: number
  name: string
  email: string
}

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(loadUsers, (state) => ({
    ...state,
    loading: true,
    error: null
  })),
  on(loadUsersSuccess, (state, { users }) => ({
    ...state,
    users,
    loading: false,
    error: null
  })),
  on(loadUsersFailure, (state, { error }) => ({
    ...state,
    users: [],
    loading: false,
    error
  }))
)

Feature State Reducer

import { createReducer, on } from '@ngrx/store'
import {
  setFilter,
  setSortBy,
  setPage,
  setSearch
} from './products.actions'

export type FilterType = 'all' | 'active' | 'inactive'
export type SortType = 'name' | 'price' | 'date'

export interface ProductsState {
  filter: FilterType
  sortBy: SortType
  page: number
  search: string
}

export const initialState: ProductsState = {
  filter: 'all',
  sortBy: 'name',
  page: 1,
  search: ''
}

export const productsReducer = createReducer(
  initialState,
  on(setFilter, (state, { filter }) => ({
    ...state,
    filter,
    page: 1 // Reset page when filter changes
  })),
  on(setSortBy, (state, { sortBy }) => ({
    ...state,
    sortBy,
    page: 1 // Reset page when sort changes
  })),
  on(setPage, (state, { page }) => ({
    ...state,
    page
  })),
  on(setSearch, (state, { search }) => ({
    ...state,
    search,
    page: 1 // Reset page when searching
  }))
)

Register Reducer in Module

import { NgModule } from '@angular/core'
import { StoreModule } from '@ngrx/store'
import { counterReducer } from './store/counter.reducer'
import { todoReducer } from './store/todo.reducer'
import { userReducer } from './store/user.reducer'

@NgModule({
  imports: [
    StoreModule.forRoot({
      counter: counterReducer,
      todos: todoReducer,
      users: userReducer
    })
  ]
})
export class AppModule {}

Multiple Actions in One Handler

import { createReducer, on } from '@ngrx/store'
import {
  loginSuccess,
  signupSuccess,
  logout,
  refreshTokenSuccess
} from './auth.actions'

export interface AuthState {
  user: { id: number; name: string } | null
  token: string | null
  isAuthenticated: boolean
}

export const initialState: AuthState = {
  user: null,
  token: null,
  isAuthenticated: false
}

export const authReducer = createReducer(
  initialState,
  // Handle multiple actions with same logic
  on(loginSuccess, signupSuccess, refreshTokenSuccess, (state, { user, token }) => ({
    ...state,
    user,
    token,
    isAuthenticated: true
  })),
  on(logout, () => initialState)
)

Testing Reducers

import { counterReducer, initialState } from './counter.reducer'
import { increment, decrement, reset } from './counter.actions'

describe('CounterReducer', () => {
  it('should return initial state', () => {
    const action = { type: 'Unknown' }
    const state = counterReducer(undefined, action)

    expect(state).toBe(initialState)
  })

  it('should increment count', () => {
    const action = increment()
    const state = counterReducer(initialState, action)

    expect(state.count).toBe(1)
  })

  it('should decrement count', () => {
    const previousState = { count: 5 }
    const action = decrement()
    const state = counterReducer(previousState, action)

    expect(state.count).toBe(4)
  })

  it('should reset count', () => {
    const previousState = { count: 10 }
    const action = reset()
    const state = counterReducer(previousState, action)

    expect(state.count).toBe(0)
  })
})

Best Practice Note

This is the same reducer pattern we use in CoreUI’s Angular admin templates with NgRx. Reducers must be pure functions that never mutate state directly - always return new state objects. Use the spread operator for immutable updates, handle loading and error states consistently, and keep reducers focused on a single slice of state. Always test reducers independently from effects and components.

For production applications, consider using CoreUI’s Angular Admin Template which includes pre-configured NgRx setup with best practices for reducers, actions, and effects.

For complete NgRx implementation, check out how to use NgRx Store in Angular, how to use actions in NgRx, and how to use selectors in NgRx.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author