How to use actions in NgRx

NgRx actions represent unique events in your Angular application that trigger state changes through reducers and side effects. As the creator of CoreUI with 12 years of Angular development experience, I’ve built enterprise NgRx applications where well-structured actions provide clear audit trails of all state mutations.

The most maintainable approach uses createAction with strongly-typed props for type-safe action creators.

Create Basic Actions

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

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

export const loadUsers = createAction('[User List] Load Users')

export const loadUsersSuccess = createAction(
  '[User API] Load Users Success',
  props<{ users: User[] }>()
)

export const loadUsersFailure = createAction(
  '[User API] Load Users Failure',
  props<{ error: string }>()
)

Dispatch Actions from Component

import { Component, OnInit } from '@angular/core'
import { Store } from '@ngrx/store'
import { loadUsers } from './store/user/user.actions'

@Component({
  selector: 'app-user-list',
  template: `
    <button (click)="onLoadUsers()">Load Users</button>
    <div *ngFor="let user of users$ | async">
      {{ user.name }}
    </div>
  `
})
export class UserListComponent implements OnInit {
  users$ = this.store.select(selectAllUsers)

  constructor(private store: Store) {}

  ngOnInit() {
    this.store.dispatch(loadUsers())
  }

  onLoadUsers() {
    this.store.dispatch(loadUsers())
  }
}

Actions with Payload

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

export const addUser = createAction(
  '[User Form] Add User',
  props<{ user: User }>()
)

export const updateUser = createAction(
  '[User Form] Update User',
  props<{ id: string; changes: Partial<User> }>()
)

export const deleteUser = createAction(
  '[User List] Delete User',
  props<{ id: string }>()
)

export const selectUser = createAction(
  '[User List] Select User',
  props<{ userId: string }>()
)

Action Naming Convention

// Format: [Source] Event
// Source: Where action originates
// Event: What happened

export const loginActions = {
  // From login form component
  login: createAction(
    '[Login Form] Login',
    props<{ username: string; password: string }>()
  ),

  // From auth API effect
  loginSuccess: createAction(
    '[Auth API] Login Success',
    props<{ user: User; token: string }>()
  ),

  loginFailure: createAction(
    '[Auth API] Login Failure',
    props<{ error: string }>()
  ),

  // From logout button
  logout: createAction('[Header] Logout')
}

Action Groups

import { createActionGroup, props, emptyProps } from '@ngrx/store'

export const UserActions = createActionGroup({
  source: 'User',
  events: {
    'Load Users': emptyProps(),
    'Load Users Success': props<{ users: User[] }>(),
    'Load Users Failure': props<{ error: string }>(),
    'Add User': props<{ user: User }>(),
    'Update User': props<{ id: string; changes: Partial<User> }>(),
    'Delete User': props<{ id: string }>()
  }
})

// Usage
this.store.dispatch(UserActions.loadUsers())
this.store.dispatch(UserActions.addUser({ user }))

Actions with Effects

Create src/app/store/user/user.effects.ts:

import { Injectable } from '@angular/core'
import { Actions, createEffect, ofType } from '@ngrx/effects'
import { of } from 'rxjs'
import { map, catchError, switchMap } from 'rxjs/operators'
import { UserService } from '@/services/user.service'
import * as UserActions from './user.actions'

@Injectable()
export class UserEffects {
  loadUsers$ = createEffect(() =>
    this.actions$.pipe(
      ofType(UserActions.loadUsers),
      switchMap(() =>
        this.userService.getUsers().pipe(
          map(users => UserActions.loadUsersSuccess({ users })),
          catchError(error =>
            of(UserActions.loadUsersFailure({ error: error.message }))
          )
        )
      )
    )
  )

  addUser$ = createEffect(() =>
    this.actions$.pipe(
      ofType(UserActions.addUser),
      switchMap(({ user }) =>
        this.userService.createUser(user).pipe(
          map(newUser => UserActions.addUserSuccess({ user: newUser })),
          catchError(error =>
            of(UserActions.addUserFailure({ error: error.message }))
          )
        )
      )
    )
  )

  constructor(
    private actions$: Actions,
    private userService: UserService
  ) {}
}

Actions in Reducers

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

import { createReducer, on } from '@ngrx/store'
import * as UserActions from './user.actions'

export interface UserState {
  users: User[]
  loading: boolean
  error: string | null
  selectedUserId: string | null
}

const initialState: UserState = {
  users: [],
  loading: false,
  error: null,
  selectedUserId: null
}

export const userReducer = createReducer(
  initialState,
  on(UserActions.loadUsers, (state) => ({
    ...state,
    loading: true,
    error: null
  })),
  on(UserActions.loadUsersSuccess, (state, { users }) => ({
    ...state,
    users,
    loading: false
  })),
  on(UserActions.loadUsersFailure, (state, { error }) => ({
    ...state,
    loading: false,
    error
  })),
  on(UserActions.addUser, (state, { user }) => ({
    ...state,
    users: [...state.users, user]
  })),
  on(UserActions.updateUser, (state, { id, changes }) => ({
    ...state,
    users: state.users.map(user =>
      user.id === id ? { ...user, ...changes } : user
    )
  })),
  on(UserActions.deleteUser, (state, { id }) => ({
    ...state,
    users: state.users.filter(user => user.id !== id)
  })),
  on(UserActions.selectUser, (state, { userId }) => ({
    ...state,
    selectedUserId: userId
  }))
)

Action Type Guards

import { Action } from '@ngrx/store'

export function isLoadUsersAction(action: Action): action is ReturnType<typeof loadUsers> {
  return action.type === loadUsers.type
}

// Usage in middleware or testing
if (isLoadUsersAction(action)) {
  console.log('Loading users...', action)
}

Conditional Actions

import { Component } from '@angular/core'
import { Store } from '@ngrx/store'
import { map, take } from 'rxjs/operators'
import { selectUserRole } from './store/selectors'
import { deleteUser } from './store/actions'

@Component({
  selector: 'app-user-item',
  template: `
    <button (click)="onDelete()" *ngIf="canDelete$ | async">
      Delete
    </button>
  `
})
export class UserItemComponent {
  canDelete$ = this.store.select(selectUserRole).pipe(
    map(role => role === 'admin')
  )

  constructor(private store: Store) {}

  onDelete() {
    this.canDelete$.pipe(take(1)).subscribe(canDelete => {
      if (canDelete) {
        this.store.dispatch(deleteUser({ id: this.user.id }))
      }
    })
  }
}

Action Testing

import { TestBed } from '@angular/core/testing'
import { provideMockStore, MockStore } from '@ngrx/store/testing'
import { UserListComponent } from './user-list.component'
import { loadUsers } from './store/user/user.actions'

describe('UserListComponent', () => {
  let store: MockStore
  let component: UserListComponent

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [UserListComponent],
      providers: [provideMockStore()]
    })

    store = TestBed.inject(MockStore)
    component = TestBed.createComponent(UserListComponent).componentInstance
  })

  it('should dispatch loadUsers on init', () => {
    spyOn(store, 'dispatch')
    component.ngOnInit()
    expect(store.dispatch).toHaveBeenCalledWith(loadUsers())
  })
})

Best Practice Note

This is the same NgRx action pattern we use in CoreUI’s Angular admin templates. Actions provide a clear contract between components and state management, making the application easier to debug and test. Always use descriptive action names with source context and strongly-typed props for compile-time safety.

For production applications, consider using CoreUI’s Angular Admin Template which includes pre-configured NgRx patterns with comprehensive action organization.

For complete state management implementation, you might also want to learn how to use selectors in NgRx and how to use effects 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

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.

Answers by CoreUI Core Team