The actions and reducers pattern helps manage app data in a clear way. It keeps track of changes step-by-step so your app stays organized and easy to fix.
Actions and reducers pattern in Angular
Start learning this pattern below
Jump into concepts and practice - no test required
import { createAction, props } from '@ngrx/store'; export const actionName = createAction( '[Source] Event', props<{ payloadProperty: any }>() ); import { createReducer, on } from '@ngrx/store'; export const initialState = { /* initial state object */ }; export const reducerName = createReducer( initialState, on(actionName, (state, { payloadProperty }) => ({ ...state, /* new state changes */ })) );
Actions describe what happened, carrying any needed data.
Reducers decide how the state changes based on actions.
import { createAction } from '@ngrx/store'; export const increment = createAction('[Counter] Increment');
import { createAction, props } from '@ngrx/store'; export const addItem = createAction( '[Todo] Add Item', props<{ item: string }>() );
import { createReducer, on } from '@ngrx/store'; export const initialState = { count: 0 }; export const counterReducer = createReducer( initialState, on(increment, state => ({ ...state, count: state.count + 1 })) );
This Angular component shows a number and a button. When you click the button, it sends an increment action. The reducer updates the count by 1. The screen updates to show the new count.
This pattern keeps the data changes clear and separate from the display.
import { Component } from '@angular/core'; import { Store, createAction, createReducer, on, props } from '@ngrx/store'; import { Observable } from 'rxjs'; // Define action export const increment = createAction('[Counter] Increment'); // Define state interface interface CounterState { count: number; } // Initial state const initialState: CounterState = { count: 0 }; // Reducer function export const counterReducer = createReducer( initialState, on(increment, state => ({ ...state, count: state.count + 1 })) ); @Component({ selector: 'app-counter', template: ` <main> <h1>Counter: {{ count$ | async }}</h1> <button (click)="incrementCount()" aria-label="Increment counter">Increment</button> </main> `, standalone: true }) export class CounterComponent { count$: Observable<number>; constructor(private store: Store<{ counter: CounterState }>) { this.count$ = store.select(state => state.counter.count); } incrementCount() { this.store.dispatch(increment()); } }
Always keep actions simple and focused on one event.
Reducers must not change the old state directly; always return a new state object.
Use descriptive action names to understand what happened easily.
Actions tell what happened, carrying needed data.
Reducers decide how state changes based on actions.
This pattern helps keep app data organized and easy to manage.
Practice
action?Solution
Step 1: Understand the purpose of actions
Actions are simple objects that describe an event that happened in the app and carry any necessary data.Step 2: Differentiate from other parts
Reducers handle state changes, not actions. UI updates and data fetching are separate concerns.Final Answer:
To describe what happened and carry data about the event -> Option AQuick Check:
Action = event description + data [OK]
- Confusing actions with reducers
- Thinking actions update UI directly
- Assuming actions hold the whole state
Solution
Step 1: Recall createAction syntax
The correct syntax is calling createAction with a string describing the action type.Step 2: Check each option
const loadItems = createAction('Load Items'); matches the correct syntax. const loadItems = createAction = 'Load Items'; uses wrong assignment. const loadItems = actionCreate('Load Items'); uses wrong function name. const loadItems = createAction('Load Items', payload); incorrectly adds a second argument without proper structure.Final Answer:
const loadItems = createAction('Load Items'); -> Option DQuick Check:
createAction('type') is correct [OK]
- Using wrong function names
- Assigning createAction instead of calling it
- Passing payload directly as second argument
{ type: 'increment' } if the initial state is { count: 0 }?
function counterReducer(state = { count: 0 }, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}Solution
Step 1: Identify the action type and initial state
The action type is 'increment' and initial state has count 0.Step 2: Follow reducer logic for 'increment'
The reducer returns a new state with count increased by 1, so count becomes 1.Final Answer:
{ count: 1 } -> Option AQuick Check:
increment adds 1 to count [OK]
- Returning old state instead of updated
- Confusing increment with decrement
- Expecting mutation instead of new object
function todoReducer(state = [], action) {
if (action.type = 'add') {
return [...state, action.payload];
}
return state;
}Solution
Step 1: Check the if condition syntax
The condition uses single equals (=) which assigns instead of compares. This causes a bug.Step 2: Verify other parts
Default case is handled by returning state. State as array is valid for todo list. Returning new array is correct for immutability.Final Answer:
Using assignment (=) instead of comparison (===) in the if condition -> Option CQuick Check:
Use '===' for comparison in conditions [OK]
- Confusing '=' with '===' in conditions
- Thinking default case is missing
- Believing state must be an object
Solution
Step 1: Define the reset action properly
Use createAction with a string type 'reset' to define the action.Step 2: Update reducer to handle reset
Add a case for 'reset' that returns a new state object with count set to 0, ensuring immutability.Final Answer:
Defineconst reset = createAction('reset');and addcase 'reset': return { count: 0 };in reducer -> Option BQuick Check:
Action + reducer case resets state immutably [OK]
- Mutating state directly in reducer
- Ignoring reducer update for new action
- Misusing createAction with payload function
