A minimal implementation of xstate fsm for UI machines
Core machine creation and setup utilities for defining finite state machines with typed schemas, guards, and transition logic.
Creates a new finite state machine from configuration with full type safety and schema validation.
/**
* Creates a new finite state machine from configuration
* @param config - Machine configuration defining states, transitions, context, and behavior
* @returns Machine configuration (use with a runtime service to execute)
*/
function createMachine<T extends MachineSchema>(config: Machine<T>): Machine<T>;
interface Machine<T extends MachineSchema> {
/** Enable debug logging for state transitions */
debug?: boolean | undefined;
/** Function that returns props object for the machine */
props?: ((params: PropsParams<T>) => T["props"]) | undefined;
/** Function that returns bindable context values */
context?: ((params: ContextParams<T>) => {
[K in keyof T["context"]]: Bindable<T["context"][K]>
}) | undefined;
/** Computed values derived from context and props */
computed?: {
[K in keyof T["computed"]]: (params: ComputedParams<T>) => T["computed"][K]
} | undefined;
/** Function that returns the initial state based on props */
initialState: (params: { prop: PropFn<T> }) => T["state"];
/** Actions to execute when entering the machine */
entry?: ActionsOrFn<T> | undefined;
/** Actions to execute when exiting the machine */
exit?: ActionsOrFn<T> | undefined;
/** Effects to run while the machine is active */
effects?: EffectsOrFn<T> | undefined;
/** Function that returns refs object for DOM elements */
refs?: ((params: RefsParams<T>) => T["refs"]) | undefined;
/** Watch function called on every state change */
watch?: ((params: Params<T>) => void) | undefined;
/** Global event handlers (fallback when state doesn't handle event) */
on?: {
[E in T["event"]["type"]]?: Transition<T> | Array<Transition<T>>
} | undefined;
/** State node definitions */
states: {
[K in T["state"]]: {
/** Tags associated with this state */
tags?: T["tag"][] | undefined;
/** Actions to execute when entering this state */
entry?: ActionsOrFn<T> | undefined;
/** Actions to execute when exiting this state */
exit?: ActionsOrFn<T> | undefined;
/** Effects to run while in this state */
effects?: EffectsOrFn<T> | undefined;
/** Event handlers for this state */
on?: {
[E in T["event"]["type"]]?: Transition<T> | Array<Transition<T>>
} | undefined;
}
};
/** Implementation functions for guards, actions, and effects */
implementations?: {
guards?: {
[K in T["guard"]]: (params: Params<T>) => boolean
} | undefined;
actions?: {
[K in T["action"]]: (params: Params<T>) => void
} | undefined;
effects?: {
[K in T["effect"]]: (params: Params<T>) => void | VoidFunction
} | undefined;
} | undefined;
}Usage Examples:
import { createMachine, MachineSchema } from "@zag-js/core";
// Simple toggle machine
interface ToggleMachine extends MachineSchema {
state: "inactive" | "active";
event: { type: "TOGGLE" };
}
const toggleMachine = createMachine<ToggleMachine>({
initialState: () => "inactive",
states: {
inactive: {
on: { TOGGLE: { target: "active" } }
},
active: {
on: { TOGGLE: { target: "inactive" } }
}
}
});
// Machine with context and actions
interface CounterMachine extends MachineSchema {
state: "idle" | "counting";
context: { count: number; step: number };
event: { type: "INCREMENT" } | { type: "DECREMENT" } | { type: "SET_STEP"; step: number };
action: "increment" | "decrement" | "setStep";
}
const counterMachine = createMachine<CounterMachine>({
initialState: () => "idle",
context: ({ bindable }) => ({
count: bindable(() => ({ defaultValue: 0 })),
step: bindable(() => ({ defaultValue: 1 }))
}),
states: {
idle: {
on: {
INCREMENT: { target: "counting", actions: ["increment"] },
DECREMENT: { target: "counting", actions: ["decrement"] },
SET_STEP: { actions: ["setStep"] }
}
},
counting: {
on: {
INCREMENT: { actions: ["increment"] },
DECREMENT: { actions: ["decrement"] }
}
}
},
implementations: {
actions: {
increment: ({ context }) => context.set("count", (prev) => prev + context.get("step")),
decrement: ({ context }) => context.set("count", (prev) => prev - context.get("step")),
setStep: ({ context, event }) => context.set("step", event.step)
}
}
});Returns setup utilities for machine creation with typed guards and transition choosers.
/**
* Returns setup utilities for machine creation with typed guards and choosers
* @returns Object with guards, createMachine, and choose utilities
*/
function setup<T extends MachineSchema>(): {
guards: GuardUtilities<T>;
createMachine: (config: Machine<T>) => Machine<T>;
choose: (transitions: Transition<T> | Transition<T>[]) => (params: Params<T>) => T["action"][] | undefined;
};
interface GuardUtilities<T extends MachineSchema> {
/** Logical AND combination of guards */
and(...guards: Array<GuardFn<T> | T["guard"]>): GuardFn<T>;
/** Logical OR combination of guards */
or(...guards: Array<GuardFn<T> | T["guard"]>): GuardFn<T>;
/** Logical NOT negation of guard */
not(guard: GuardFn<T> | T["guard"]): GuardFn<T>;
}Usage Examples:
import { setup, MachineSchema } from "@zag-js/core";
interface FormMachine extends MachineSchema {
state: "idle" | "validating" | "valid" | "invalid";
context: { email: string; isValid: boolean };
guard: "isEmailValid" | "isEmailEmpty";
action: "validate" | "clear";
}
const { guards, createMachine } = setup<FormMachine>();
const formMachine = createMachine({
initialState: () => "idle",
states: {
idle: {
on: {
VALIDATE: {
target: "validating",
guard: guards.not("isEmailEmpty")
}
}
},
validating: {
entry: ["validate"],
on: {
VALIDATION_COMPLETE: [
{ target: "valid", guard: "isEmailValid" },
{ target: "invalid", guard: guards.not("isEmailValid") }
]
}
},
valid: {
on: { CLEAR: { target: "idle", actions: ["clear"] } }
},
invalid: {
on: { CLEAR: { target: "idle", actions: ["clear"] } }
}
},
implementations: {
guards: {
isEmailValid: ({ context }) => /\S+@\S+\.\S+/.test(context.get("email")),
isEmailEmpty: ({ context }) => context.get("email").trim() === ""
},
actions: {
validate: ({ context, send }) => {
// Simulate async validation
setTimeout(() => send({ type: "VALIDATION_COMPLETE" }), 100);
},
clear: ({ context }) => context.set("email", "")
}
}
});Creates logical guard combinations for complex conditional logic in state transitions.
/**
* Creates logical guard combinations (and, or, not)
* @returns Object with logical guard combination functions
*/
function createGuards<T extends MachineSchema>(): GuardUtilities<T>;Usage Examples:
import { createGuards, MachineSchema } from "@zag-js/core";
interface AuthMachine extends MachineSchema {
state: "idle" | "loading" | "success" | "error";
context: { user: User | null; attempts: number };
guard: "isLoggedIn" | "hasMaxAttempts" | "isValidUser";
action: "login" | "logout" | "incrementAttempts";
}
const guards = createGuards<AuthMachine>();
// Using guard combinations
const loginGuard = guards.and("isValidUser", guards.not("hasMaxAttempts"));
const logoutGuard = guards.or("isLoggedIn", guards.not("isValidUser"));interface Transition<T extends MachineSchema> {
/** Target state to transition to */
target?: T["state"] | undefined;
/** Actions to execute during transition */
actions?: T["action"][] | undefined;
/** Guard condition that must pass for transition */
guard?: T["guard"] | GuardFn<T> | undefined;
/** Allow re-entering the same state */
reenter?: boolean | undefined;
}
type ActionsOrFn<T extends MachineSchema> =
T["action"][] | ((params: Params<T>) => T["action"][] | undefined);
type EffectsOrFn<T extends MachineSchema> =
T["effect"][] | ((params: Params<T>) => T["effect"][] | undefined);
type GuardFn<T extends MachineSchema> = (params: Params<T>) => boolean;
interface PropsParams<T extends MachineSchema> {
props: Partial<T["props"]>;
scope: Scope;
}
interface ContextParams<T extends MachineSchema> {
prop: PropFn<T>;
bindable: BindableFn;
scope: Scope;
getContext: () => BindableContext<T>;
getComputed: () => ComputedFn<T>;
getRefs: () => BindableRefs<T>;
getEvent: () => EventType<T["event"]>;
flush: (fn: VoidFunction) => void;
}
interface ComputedParams<T extends MachineSchema> {
context: BindableContext<T>;
event: EventType<T["event"]>;
prop: PropFn<T>;
refs: BindableRefs<T>;
scope: Scope;
computed: ComputedFn<T>;
}
interface RefsParams<T extends MachineSchema> {
prop: PropFn<T>;
context: BindableContext<T>;
}Install with Tessl CLI
npx tessl i tessl/npm-zag-js--core