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 statetransitionTo(state: FSMContextState<TSelf>): Promise<void>- Transition to a new state
FSMState
name: TName- State name (unique identifier)isFinal: boolean- Whether this is a final statehandleNext(context: TContext, payload: TPayload): Promise<void>- Handle actions dispatched to this stateonEnter?(context: TContext): Promise<void>- Called when entering this state
Type Helpers
FSMStateConfig<TName, TPayload>- Define a state configurationFSMContextState<TContext>- Extract state type from contextFSMContext<TConfig>- Context interface for state handlers
Best Practices
- Always call
initialize(): The FSM must be initialized before dispatching actions - Use
onEnterfor side effects: Perform initialization or automatic transitions when entering a state - Mark final states: Set
isFinal: truefor states that don't accept further transitions - Type safety: Use TypeScript's type system to ensure only valid payloads are dispatched to each state
- Reactive state: Subscribe to
currentStateto react to state changes in React components