core

Deep Dive into @ngx-axon/core: The Neural Pathway for Angular State Management

Welcome! If you are looking to build robust, deterministic, and predictable state architectures inside your modern Angular applications, you’ve come to the right place.

This article outlines how @ngx-axon/core solves common architectural friction points in Angular, followed by a step-by-step tutorial demonstrating everything from simple state transitions to advanced RxJS composition.


Introduction: The Limits of the Competition

For years, state management in Angular has felt like a tug-of-war between two extremes:

  1. Global Redux-like Patterns (NgRx Store): While incredibly powerful for massive enterprise enterprise applications, global stores bring immense boilerplate (actions, selectors, reducers, effects). Attempting to utilize them for transient, component-level workflows or isolated UI states quickly turns into an over-engineered maintenance burden.
  2. Heavy-duty State Chart Frameworks (XState): XState is an exceptional industry standard for complex statecharts. However, its engine is framework-agnostic. Bringing it into modern Angular requires external wrapper abstractions, creates friction when syncing deeply nested state objects with native Angular Signals, and introduces significant bundle weight.

How ngx-axon Overcomes Them

@ngx-axon/core acts as a specialized neural pathway for your components. Designed natively for modern Angular, it provides a lightweight, highly-optimized Finite State Machine (FSM) engine built directly on top of Angular Signals.


Step-by-Step Tutorial

Let’s explore how to implement @ngx-axon/core in real-world scenarios. Start by installing it with:

npm install @ngx-axon/core

1. Simple Transitions

A simple transition moves your machine from an initial state directly to a targeted state via an explicit event trigger. Think of an interactive video player switching between paused and playing.

import { Injectable, signal } from '@angular/core';
import { createMachine } from '@ngx-axon/core';

@Injectable({ providedIn: 'root' })
export class VideoPlayerService {
  // Define a lightweight FSM
  private playerMachine = createMachine({
    id: 'videoPlayer',
    initial: 'paused',
    states: {
      paused: {
        on: { PLAY: 'playing' }
      },
      playing: {
        on: { PAUSE: 'paused' }
      }
    }
  });

  // Expose the state natively as an Angular Signal
  readonly currentState = this.playerMachine.state;

  play() {
    this.playerMachine.transition('PLAY');
  }

  pause() {
    this.playerMachine.transition('PAUSE');
  }
}


2. Guarded Transitions

Often, you don’t want a transition to execute unless certain business rules or constraints are met. ngx-axon introduces guard predicates to evaluate conditional entry.

Below, a user cannot transition from cart to processing if their checkout inventory is empty.

import { Injectable, signal, computed } from '@angular/core';
import { createMachine } from '@ngx-axon/core';

@Injectable({ providedIn: 'root' })
export class CheckoutService {
  // Context to hold dynamic values
  cartItems = signal<{ id: string; price: number }[]>([]);

  private checkoutMachine = createMachine({
    id: 'checkoutWizard',
    initial: 'cart',
    states: {
      cart: {
        on: {
          SUBMIT_ORDER: {
            target: 'processing',
            // The guard must return true for the transition to occur
            guard: () => this.cartItems().length > 0
          }
        }
      },
      processing: {
        on: {
          SUCCESS: 'completed',
          FAIL: 'cart'
        }
      },
      completed: {}
    }
  });

  readonly step = this.checkoutMachine.state;

  nextStep() {
    this.checkoutMachine.transition('SUBMIT_ORDER');
  }
  
  addItem(item: { id: string; price: number }) {
    this.cartItems.update(items => [...items, item]);
  }
}


3. Transitions with Side Effects

When a transition successfully executes, you frequently need to run localized side effects—such as calling an analytics tracking engine, dispatching HTTP calls, or triggering toast notifications.

import { inject, Injectable } from '@angular/core';
import { createMachine } from '@ngx-axon/core';
import { NotificationService } from './notification.service';

@Injectable({ providedIn: 'root' })
export class AuthStateMachineService {
  private toaster = inject(NotificationService);

  private authMachine = createMachine({
    id: 'auth',
    initial: 'loggedOut',
    states: {
      loggedOut: {
        on: {
          LOGIN_SUBMIT: 'authenticating'
        }
      },
      authenticating: {
        on: {
          LOGIN_SUCCESS: {
            target: 'loggedIn',
            // Side effects executed cleanly upon transition completion
            actions: [
              () => this.toaster.showSuccess('Welcome back!'),
              (event) => console.log('User metadata logged:', event.payload)
            ]
          },
          LOGIN_ERROR: {
            target: 'loggedOut',
            actions: [
              () => this.toaster.showError('Authentication failed. Please retry.')
            ]
          }
        }
      },
      loggedIn: {}
    }
  });

  readonly status = this.authMachine.state;

  loginSuccess(userPayload: any) {
    this.authMachine.transition('LOGIN_SUCCESS', userPayload);
  }
}


4. RxJS Interop: Deriving Top-Level Status from Lower-Level Statuses

One of the most elegant architectural patterns supported by ngx-axon is the ability to break complex layouts down into multiple, smaller micro-machines (lower-level statuses) and unify them using Angular’s native RxJS interop utilities (toObservable and toSignal).

Consider a File Management Dashboard (frequently showcased in the repository’s examples). Instead of building one monstrous state machine managing your file validation, local compression, and AWS network pipeline simultaneously, we can decouple them into dedicated micro-machines and derive a consolidated global status block.

import { Component, inject } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { combineLatest, map } from 'rxjs';
import { createMachine } from '@ngx-axon/core';

@Component({
  selector: 'app-file-uploader',
  standalone: true,
  template: `
    <div class="dashboard">
      <h3>Global Pipeline Status: </h3>
      
      <p>File Verification: </p>
      <p>Cloud Storage Pipe: </p>
    </div>
  `
})
export class FileUploaderComponent {
  // Low-level Machine 1: File Structural Checks
  validationMachine = createMachine({
    initial: 'idle',
    states: {
      idle: { on: { START: 'validating' } },
      validating: { on: { VALID: 'passed', INVALID: 'failed' } },
      passed: {},
      failed: {}
    }
  });

  // Low-level Machine 2: Network Multipart Uploader
  uploadMachine = createMachine({
    initial: 'idle',
    states: {
      idle: { on: { TRANSMIT: 'uploading' } },
      uploading: { on: { COMPLETE: 'finished', ERROR: 'failed' } },
      finished: {},
      failed: {}
    }
  });

  // Convert lower-level Signal states into RxJS Observables
  private validation$ = toObservable(this.validationMachine.state);
  private upload$ = toObservable(this.uploadMachine.state);

  // Use RxJS to intelligently compose and derive a macro-level status
  private derivedStatus$ = combineLatest([this.validation$, this.upload$]).pipe(
    map(([validState, uploadState]) => {
      if (validState === 'failed' || uploadState === 'failed') {
        return 'PIPELINE_ERROR';
      }
      if (validState === 'validating') {
        return 'PROCESSING_METADATA';
      }
      if (uploadState === 'uploading') {
        return 'UPLOADING_TO_CLOUD';
      }
      if (uploadState === 'finished' && validState === 'passed') {
        return 'TRANSACTION_SUCCESSFUL';
      }
      return 'SYSTEM_READY';
    })
  );

  // Stream the calculated value back into a clean, read-only UI Signal
  readonly globalDashboardStatus = toSignal(this.derivedStatus$, { 
    initialValue: 'SYSTEM_READY' 
  });
}

Summary

By leveraging @ngx-axon/core, you ensure your templates react purely to simple top-level constraints while keeping your internal transition paths clean, verifiable, and highly scalable. Try replacing your deeply conditional boolean flags with an axon pathway today!