Skip to main content

Angular NgRx Actions

Introduction

Actions are one of the fundamental building blocks in NgRx, Angular's Redux-inspired state management library. Think of actions as messenger objects that carry information from your application to the NgRx store. They describe what happened in your application, but don't specify how the application's state should change in response. Each action contains a type that identifies the action and an optional payload that carries any data needed for state changes.

In this tutorial, we'll explore NgRx actions in depth - what they are, how to create them, and best practices for implementing them in your Angular applications.

Understanding NgRx Actions

What is an Action?

An NgRx action is a plain JavaScript object that conforms to the following interface:

typescript
interface Action {
type: string;
// optional payload of any type
[key: string]: any;
}

Every action must have a type property that describes what the action represents. The type is typically defined as a string constant to avoid typos and duplication. Additionally, actions may include a payload containing data relevant to the action.

Action Structure

Let's look at a basic action example:

typescript
import { Action } from '@ngrx/store';

export const INCREMENT = '[Counter] Increment';

export class IncrementAction implements Action {
readonly type = INCREMENT;
}

This simple action defines an increment operation for a counter. The naming convention '[Source] Event' is recommended for action types:

  • Source: The feature or component that generated the action
  • Event: What happened (e.g., increment, load, add)

Creating Actions with createAction

While the class-based approach shown above works, NgRx 8+ introduced a more concise way to define actions using the createAction function:

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

export const increment = createAction('[Counter] Increment');
export const decrement = createAction('[Counter] Decrement');
export const reset = createAction('[Counter] Reset');

For actions that need to carry additional information, we can use the props function:

typescript
export const incrementBy = createAction(
'[Counter] Increment By',
props<{ amount: number }>()
);

export const setCounter = createAction(
'[Counter] Set',
props<{ value: number }>()
);

Dispatching Actions

Actions are dispatched to the store using the dispatch method. Here's how you would dispatch an action from a component:

typescript
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import * as CounterActions from '../actions/counter.actions';

@Component({
selector: 'app-counter',
template: `
<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
<button (click)="reset()">Reset</button>
<button (click)="incrementByFive()">Increment by 5</button>
`
})
export class CounterComponent {
constructor(private store: Store) {}

increment() {
this.store.dispatch(CounterActions.increment());
}

decrement() {
this.store.dispatch(CounterActions.decrement());
}

reset() {
this.store.dispatch(CounterActions.reset());
}

incrementByFive() {
this.store.dispatch(CounterActions.incrementBy({ amount: 5 }));
}
}

Action Best Practices

1. Use the Action Creator Pattern

Using createAction makes your code more readable and maintainable:

typescript
// DO ✅
export const loginSuccess = createAction(
'[Auth] Login Success',
props<{ user: User }>()
);

// DON'T ❌
export class LoginSuccessAction implements Action {
readonly type = '[Auth] Login Success';
constructor(public user: User) {}
}

2. Group Actions by Feature

Keep your actions organized by grouping them based on feature:

typescript
// auth.actions.ts
export const login = createAction(
'[Auth] Login',
props<{ username: string, password: string }>()
);
export const loginSuccess = createAction(
'[Auth] Login Success',
props<{ user: User }>()
);
export const loginFailure = createAction(
'[Auth] Login Failure',
props<{ error: string }>()
);

// user.actions.ts
export const loadUsers = createAction('[User] Load Users');
export const loadUsersSuccess = createAction(
'[User] Load Users Success',
props<{ users: User[] }>()
);

3. Use Descriptive Action Types

Make your action types descriptive and follow the [Source] Event pattern:

typescript
// DO ✅
export const addTodo = createAction(
'[Todo List] Add Todo',
props<{ text: string }>()
);

// DON'T ❌
export const ADD = createAction('ADD', props<{ text: string }>());

4. Keep Actions Pure and Simple

Actions should be simple messengers and shouldn't contain business logic:

typescript
// DO ✅
export const updateUser = createAction(
'[User] Update User',
props<{ id: number, changes: Partial<User> }>()
);

// DON'T ❌ - Don't include complex logic in action creation
export const updateUserAndRecalculate = createAction(...);

Real-World Example: Todo Application

Let's implement a more comprehensive example for a Todo application:

1. Define the Todo Model

typescript
// models/todo.model.ts
export interface Todo {
id: number;
text: string;
completed: boolean;
}

2. Create Todo Actions

typescript
// actions/todo.actions.ts
import { createAction, props } from '@ngrx/store';
import { Todo } from '../models/todo.model';

export const loadTodos = createAction('[Todo] Load Todos');
export const loadTodosSuccess = createAction(
'[Todo] Load Todos Success',
props<{ todos: Todo[] }>()
);
export const loadTodosFailure = createAction(
'[Todo] Load Todos Failure',
props<{ error: string }>()
);

export const addTodo = createAction(
'[Todo] Add Todo',
props<{ text: string }>()
);
export const addTodoSuccess = createAction(
'[Todo] Add Todo Success',
props<{ todo: Todo }>()
);

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

export const deleteTodo = createAction(
'[Todo] Delete Todo',
props<{ id: number }>()
);

3. Use Actions in a Component

typescript
// components/todo-list.component.ts
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { Todo } from '../models/todo.model';
import * as TodoActions from '../actions/todo.actions';

@Component({
selector: 'app-todo-list',
template: `
<div class="todo-container">
<h2>Todo List</h2>

<div class="add-todo">
<input #todoInput type="text" placeholder="Add new todo">
<button (click)="addTodo(todoInput.value); todoInput.value=''">
Add Todo
</button>
</div>

<div *ngIf="(todos$ | async) as todos">
<div *ngFor="let todo of todos" class="todo-item">
<input
type="checkbox"
[checked]="todo.completed"
(change)="toggleTodo(todo.id)"
>
<span [class.completed]="todo.completed">{{ todo.text }}</span>
<button (click)="deleteTodo(todo.id)">Delete</button>
</div>
</div>
</div>
`,
styles: [`
.completed { text-decoration: line-through; }
.todo-item { margin: 8px 0; }
`]
})
export class TodoListComponent implements OnInit {
todos$: Observable<Todo[]>;

constructor(private store: Store<{ todos: Todo[] }>) {
this.todos$ = store.select('todos');
}

ngOnInit() {
this.store.dispatch(TodoActions.loadTodos());
}

addTodo(text: string) {
if (text.trim()) {
this.store.dispatch(TodoActions.addTodo({ text }));
}
}

toggleTodo(id: number) {
this.store.dispatch(TodoActions.toggleTodo({ id }));
}

deleteTodo(id: number) {
this.store.dispatch(TodoActions.deleteTodo({ id }));
}
}

Action Grouping and API Integration

For larger applications, it's common to have actions related to API calls. Here's how you might organize them:

typescript
// User feature actions for API operations
export const getUsers = createAction('[User/API] Get Users');
export const getUsersSuccess = createAction(
'[User/API] Get Users Success',
props<{ users: User[] }>()
);
export const getUsersFailure = createAction(
'[User/API] Get Users Failure',
props<{ error: string }>()
);

// User UI actions
export const selectUser = createAction(
'[User/UI] Select User',
props<{ userId: string }>()
);
export const clearSelectedUser = createAction('[User/UI] Clear Selected User');

This pattern separates API-related actions from UI actions, making it easier to understand the flow of your application.

Summary

NgRx actions are the entry point for all state changes in your application. They:

  1. Define what happened in your application
  2. Carry necessary data as a payload
  3. Follow a consistent naming convention [Source] Event
  4. Should be kept pure and simple

By following best practices like using the createAction function, grouping actions by feature, and keeping them descriptive and focused, you'll create a maintainable and scalable state management architecture for your Angular applications.

Additional Resources

Exercises

  1. Create a set of actions for a simple authentication system (login, logout, register).
  2. Refactor an existing component to use NgRx actions for state management.
  3. Implement actions for a shopping cart feature (add item, remove item, update quantity, clear cart).
  4. Create actions that represent the CRUD operations for a resource of your choice.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)