NgRx store reducers
- Action’s only responsibilities are to express unique events and intents.
- Action’s does not manipulate data. This is where reducer comes into play.
- Reducers are responsible for handling transitions from one state to the next state in your application. In simpler terms, reducers are responsible for manipulating data in store.
- Reducer functions handle these transitions by determining which
actions
to handle based on the action’stype
.
Reducers are pure functions meaning they produce same output for a given input, and they handle state transition synchronously.
Defining the state shape
Create a new action file viz todo.reducer.ts
inside store
folder of todo
.
import {Todo} from '../todo.model';
import * as TodoActions from './todo.actions';
import {createReducer, on} from '@ngrx/store';
export interface TodoState {
todos: Todo[];
}
You define the shape of the state according to what you are capturing, whether it be a single type such as a number, or a more complex object with multiple properties. In our case it’s list of todos.
Setting the initial state
The initial state gives the state an initial value, or provides a value if the current state is undefined. Set our todo initial state as empty list.
export const todoInitialState: TodoState = {
todos: []
};
Creating the reducer function
The reducer function’s responsibility is to handle the state transitions in an immutable way.
Let’s define a reducer function to handle changes in the todo value based on the provided actions.
Also install to see, how to maintain immutable for complex nested objects.
export const todoFeatureKey = 'todoState';
export const todoReducer = createReducer(
todoInitialState,
on(TodoActions.saveOrUpdateTodo, (state, payload) => {
const todos = payload.isUpdate
? state.todos.map(todo => todo.id === payload.todo.id ? {...todo, done: !todo.done} : todo)
: [...state.todos, payload.todo];
return {...state, todos};
}),
on(TodoActions.deleteTodo, (state, payload) => {
const todo = state.todos.find(todo => todo.id === payload.todoId);
if (todo) {
const stateCopy = structuredClone(state);
const index = stateCopy.todos.findIndex(todo => todo.id === payload.todoId);
stateCopy.todos.splice(index, 1)
return {...state, todos: stateCopy.todos}
}
return state;
})
);
- In the example above, the reducer is handling two actions:
[Todo Component] Update Todo
and[Todo Component] Delete Todo
. - Each action is strongly-typed.
- Each action handles the state transition immutably.
- This means that the state transitions are not modifying the original state, but are returning a new state object using the spread operator.
- The spread syntax copies the properties from the current state into the object, creating a new reference. This ensures that a new state is produced with each change, preserving the purity of the change. This also promotes referential integrity, guaranteeing that the old reference was discarded when a state change occurred.
- Spread operator only does shallow copying and does not handle deeply nested objects.
- If your state has complex nested objects you may need to copy each level in the object to ensure immutability.
- For example, you can do this using
structuredClone()
.
- For example, you can do this using
When an action is dispatched, all registered reducers receive the action. Whether they handle the action is determined by the on functions that associate one or more actions with a given state change.