@ngx-axon/core: The Neural Pathway for Angular State ManagementWelcome! 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.
For years, state management in Angular has felt like a tug-of-war between two extremes:
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.
Signal<State>). This ensures lightning-fast change detection updates without manual zone interventions or heavy stream subscriptions.Let’s explore how to implement @ngx-axon/core in real-world scenarios. Start by installing it with:
npm install @ngx-axon/core
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');
}
}
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]);
}
}
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);
}
}
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'
});
}
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!