Skip to main content

What is a state machine?

A state machine is a tool for modeling stateful, reactive systems. It is useful for declaratively describing the behavior of an application or component.

To model any logic as using the state machine pattern, it must have:

  • A finite number of states.
  • A finite number of transitions between states.

Creating a state machine

Consider a simple toggle or switch component that consists of two states, active and inactive. The initial state will be active

The supported transitions that can happen here:

  • In the active state, when we click the toggle, it should transition to the inactive state
  • In the inactive state, when we click the toggle, it should transition to the active state

Here's how we'll model the logic in code:

import { createMachine } from "@zag-js/core" const machine = createMachine({ // initial state initialState() { return "active" }, // the finite states states: { active: { on: { CLICK: { // go to inactive target: "inactive", }, }, }, inactive: { on: { CLICK: { // go to active target: "active", }, }, }, }, })

TypeScript Guide

For TypeScript projects, you can add type definitions to make your machine type-safe:

import { createMachine, type Service } from "@zag-js/core" import type { NormalizeProps, PropTypes } from "@zag-js/types" interface ToggleSchema { state: "active" | "inactive" event: { type: "CLICK" } } export const machine = createMachine<ToggleSchema>({ // ... same as above })

Writing the connect function

Now that we've modelled the component logic, let's map that to DOM attributes and event handlers following the WAI-ARIA specification for the switch component.

We'll write a function called connect to do this.

function connect(service, normalize) { const { state, send } = service const active = state.matches("active") return { active, getButtonProps() { return normalize.button({ type: "button", role: "switch", "aria-checked": active, onClick() { send({ type: "CLICK" }) }, }) }, } }

TypeScript Guide

For TypeScript projects, you can add type definitions to make your connect function type-safe:

import { type Service } from "@zag-js/core" import type { NormalizeProps, PropTypes } from "@zag-js/types" interface ToggleSchema { state: "active" | "inactive" event: { type: "CLICK" } } export function connect<T extends PropTypes>( service: Service<ToggleSchema>, normalize: NormalizeProps<T>, ) { const { state, send } = service const active = state.matches("active") return { active, getButtonProps() { return normalize.button({ type: "button", role: "switch", "aria-checked": active, onClick() { send({ type: "CLICK" }) }, }) }, } }

Consuming the state machine

Here's how to consume the toggle machine logic and connect in React.js.

import { useMachine, normalizeProps } from "@zag-js/react" import { machine, connect } from "./toggle" function Toggle() { const service = useMachine(machine) const api = connect(service, normalizeProps) return ( <button {...api.getButtonProps()} style={{ width: "40px", height: "24px", borderRadius: "999px", background: api.active ? "green" : "gray", }} > {api.active ? "ON" : "OFF"} </button> ) }

That's it! Now you've learned the fundamentals of a component state machine.

Edit this page on GitHub