Skip to main content
Version: v1.0

Finite State Machine (FSM)

The FSM prefab provides a type-safe way to manage application state through well-defined states and transitions. It's particularly useful for managing complex flows like authentication, wizards, or multi-step processes.

Example: Authentication Flow

import {
InitializingState,
type InitializingConfig,
} from "./states/initializing";
import type { LoginConfig } from "./states/login";
import type { AuthenticatedConfig } from "./states/authenticated";
import {
FSMContextManager,
type FSMContextState,
} from "@dxbox/use-less-react/classes";

export type AuthConfig = InitializingConfig & LoginConfig & AuthenticatedConfig;

export class AuthFlowManager extends FSMContextManager<AuthConfig> {
constructor(initialState?: FSMContextState<AuthFlowManager>) {
super(initialState ?? new InitializingState());
}
}

State: Initializing

import type { FSMState, FSMStateConfig } from "@dxbox/use-less-react/classes";
import type { AuthFlowManager } from "../auth-flow";
import { LoginState } from "./login";

export type InitializingPayload = { intent: "initialize" };

export type InitializingConfig = FSMStateConfig<
"initializing",
InitializingPayload
>;

export class InitializingState
implements FSMState<AuthFlowManager, keyof InitializingConfig>
{
handleNext(
_context: AuthFlowManager,
_payload: InitializingPayload,
): Promise<void> {
throw new Error("Method not implemented.");
}
name = "initializing" as const;

isFinal = false;

async onEnter(context: AuthFlowManager): Promise<void> {
// simulate checking session
await new Promise((resolve) => setTimeout(resolve, 100));
await context.transitionTo(new LoginState());
}
}

State: Login

import type { FSMState, FSMStateConfig } from "@dxbox/use-less-react/classes";
import type { AuthFlowManager } from "../auth-flow";
import { AuthenticatedState } from "./authenticated";

export type LoginPayload =
| {
intent: "submit";
email: string;
password: string;
}
| {
intent: "recovery";
email: string;
};

export type LoginConfig = FSMStateConfig<"login", LoginPayload>;

export class LoginState
implements FSMState<AuthFlowManager, keyof LoginConfig>
{
name = "login" as const;

isFinal = false;

async handleNext(
context: AuthFlowManager,
payload: LoginPayload,
): Promise<void> {
switch (payload.intent) {
case "submit":
await new Promise((resolve) => setTimeout(resolve, 100));
await context.transitionTo(new AuthenticatedState());
break;
default:
throw new Error(`Method ${payload.intent} not implemented.`);
}
}
}

State: Authenticated

import type { FSMState, FSMStateConfig } from "@dxbox/use-less-react/classes";
import type { AuthFlowManager } from "../auth-flow";

export type AuthenticatedPayload = { [key: string]: never };

export type AuthenticatedConfig = FSMStateConfig<
"authenticated",
AuthenticatedPayload
>;

export class AuthenticatedState
implements FSMState<AuthFlowManager, keyof AuthenticatedConfig>
{
handleNext(
_context: AuthFlowManager,
_payload: AuthenticatedPayload,
): Promise<void> {
throw new Error("Method not implemented.");
}

name = "authenticated" as const;
isFinal = true;
}

Usage

// Create FSM instance
const authFlow = new AuthFlowManager();

// Must call initialize() first
await authFlow.initialize();

// Get current state
const currentState = authFlow.currentState.get();
console.log(currentState.name); // "login"

// Dispatch actions
await authFlow.dispatch<"login">({
intent: "submit",
email: "user@example.com",
password: "password123",
});

// State transitions automatically
console.log(authFlow.currentState.get().name); // "authenticated"

API

FSMContextManager

  • currentState: ReactiveObject<FSMContextState<TSelf>> - Current state (reactive)
  • initialize(): Promise<TSelf> - Initialize the FSM (must be called first)
  • dispatch<T extends keyof TConfig>(payload: TConfig[T]): Promise<void> - Dispatch an action to the current state
  • transitionTo(state: FSMContextState<TSelf>): Promise<void> - Transition to a new state

FSMState

  • name: TName - State name (unique identifier)
  • isFinal: boolean - Whether this is a final state
  • handleNext(context: TContext, payload: TPayload): Promise<void> - Handle actions dispatched to this state
  • onEnter?(context: TContext): Promise<void> - Called when entering this state

Type Helpers

  • FSMStateConfig<TName, TPayload> - Define a state configuration
  • FSMContextState<TContext> - Extract state type from context
  • FSMContext<TConfig> - Context interface for state handlers

Best Practices

  1. Always call initialize(): The FSM must be initialized before dispatching actions
  2. Use onEnter for side effects: Perform initialization or automatic transitions when entering a state
  3. Mark final states: Set isFinal: true for states that don't accept further transitions
  4. Type safety: Use TypeScript's type system to ensure only valid payloads are dispatched to each state
  5. Reactive state: Subscribe to currentState to react to state changes in React components