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.
Related Articles
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.



