Traffic Light State Machine
Multi-state machine with meta.route on every state, demonstrating cyclic transitions and formatPlayRouteTransitions.
Use Case
This example models a traffic light with a red → green → yellow → red cycle. Because traffic lights map naturally to URLs (each light colour is a distinct page in a web app), it is a good vehicle for showing how meta.route and formatPlayRouteTransitions work together.
Applicable patterns:
- Traffic light controllers
- Multi-step workflows with repeating cycles
- Tab or wizard navigation where each step has a URL
- Game state loops with distinct URL states
Complete Code
import { setup } from "xstate";import { definePlayer, formatPlayRouteTransitions } from "@xmachines/play-xstate";
// 1. Typed setupconst trafficSetup = setup({ types: { context: {} as { params: Record<string, string>; query: Record<string, string>; }, events: {} as | { type: "timer" } | { type: "play.route"; to: string; params?: Record<string, string>; query?: Record<string, string>; }, input: {} as undefined, },});
// 2. Machine with meta.route on every state// formatPlayRouteTransitions() reads each state's id + meta.route and// auto-generates the root-level play.route event handlers.const trafficMachine = trafficSetup.createMachine( formatPlayRouteTransitions({ id: "traffic", initial: "red", context: { params: {}, query: {}, }, states: { red: { id: "red", meta: { route: "/red" }, on: { timer: "green" }, }, green: { id: "green", meta: { route: "/green" }, on: { timer: "yellow" }, }, yellow: { id: "yellow", meta: { route: "/yellow" }, on: { timer: "red" }, }, }, }),);
// 3. Player factoryconst createPlayer = definePlayer({ machine: trafficMachine });
// 4. Create and start actorconst actor = createPlayer();actor.start();
// 5. Read current route via signalconsole.log(actor.currentRoute.get()); // "/red"
// 6. Advance with domain eventsactor.send({ type: "timer" });console.log(actor.currentRoute.get()); // "/green"
actor.send({ type: "timer" });console.log(actor.currentRoute.get()); // "/yellow"
actor.send({ type: "timer" });console.log(actor.currentRoute.get()); // "/red"
// 7. Or navigate directly via play.route events (generated by formatPlayRouteTransitions)actor.send({ type: "play.route", to: "#green" });console.log(actor.currentRoute.get()); // "/green"
// 8. Cleanupactor.stop();How formatPlayRouteTransitions Works
formatPlayRouteTransitions crawls every state that has both an id and a meta.route, then generates root-level play.route event handlers of the form:
// Auto-generated by formatPlayRouteTransitions — you don't write this manually:on: { "play.route": [ { target: ".red", guard: ({ event }) => event.to === "#red", reenter: true, actions: assign({ params, query }) }, { target: ".green", guard: ({ event }) => event.to === "#green", reenter: true, actions: assign({ params, query }) }, { target: ".yellow", guard: ({ event }) => event.to === "#yellow", reenter: true, actions: assign({ params, query }) }, ],}Requirements:
- Every routable state must have both
id(the#idnavigation target) andmeta.route(the URL template). - The machine’s context must include
paramsandqueryfields (bothRecord<string, string>), becauseformatPlayRouteTransitionsassigns them on everyplay.routetransition.
actor.currentRoute Signal
actor.currentRoute is a Signal.Computed<string | null> derived from the active state’s meta.route template and context.params. Route signals let router bridges observe URL changes reactively without polling.
import { watchSignal } from "@xmachines/play-signals";
const unwatch = watchSignal(actor.currentRoute, () => { console.log("URL changed to:", actor.currentRoute.get());});
// later...unwatch();Key Concepts
meta.route: Marks a state as routable. The value is a URL template (e.g."/profile/:username").formatPlayRouteTransitions: Utility that auto-generatesplay.routehandlers fromid+meta.routepairs. Wrap your machine config before passing tocreateMachine.actor.currentRoute: TC39Signal.Computedcontaining the resolved URL for the active state.play.routeevents: Navigation events with ato: "#stateId"target. Useto: "#red"(state ID), notto: "/red"(URL path).
Next Steps
- Form Validation Example — Context mutations with
setup.assignand guards - Routing Patterns — Parameter routes, relative routes, and
alwaysauth guards - Play RFC — Complete architectural specification