Extended Traffic Light
An extended traffic light state machine that adds pedestrian crossing functionality, demonstrating how to model more complex systems with additional states and transitions.
Implementation Details
Section titled “Implementation Details”This example builds on the basic Traffic Light example by adding:
- A new state for pedestrian crossing (
RedWithPedestrian
) - Additional state data for pedestrian signals
- A pedestrian button transition to request crossing
- Visual indication of pedestrian crossing status
- Special states for emergencies (flashing yellow) and malfunctions (flashing red)
The extended machine shows how to model a system that responds to external input (pedestrian button) and has different behaviors based on its current state.
Machine Code
Section titled “Machine Code”import { defineStates } from "matchina";
const pedestrianStates = defineStates({ Walk: undefined, DontWalk: undefined, Error: undefined,});export interface CommonStateProps { key: string; crossingRequested?: boolean; walkWarningDuration?: number;}export const sharedStates = defineStates({ State: ( props: CommonStateProps = { key: "State", crossingRequested: false } ) => props,});export const states = defineStates({ Green: () => ({ message: "Go", duration: 4000, // 4 seconds pedestrian: pedestrianStates.Walk(), }), Yellow: () => ({ message: "Prepare to stop", duration: 2000, // 2 seconds pedestrian: pedestrianStates.Walk(), }), Red: () => ({ message: "Stop", duration: 4000, // 4 seconds pedestrian: pedestrianStates.DontWalk(), }), RedWithPedestrianRequest: () => ({ message: "Stop with pedestrian requesting crossing", duration: 2000, // 2 seconds pedestrian: pedestrianStates.DontWalk(), }), FlashingYellow: () => ({ message: "Proceed with caution", duration: 0, // No automatic transition pedestrian: pedestrianStates.DontWalk(), isFlashing: true, }), FlashingRed: () => ({ message: "Stop and proceed when safe", duration: 0, // No automatic transition pedestrian: pedestrianStates.Error(), isFlashing: true, }), Broken: () => ({ message: "Broken (flashing red)", duration: 0, pedestrian: pedestrianStates.Error(), }),});
import { createMachine, enter, setup, whenState, addEventApi } from "matchina";import { states, sharedStates, type CommonStateProps } from "./states";
export const walkDuration = states.Green().data.duration + states.Yellow().data.duration;
export const greenWalkWarnAt = walkDuration - states.Yellow().data.duration - states.Green().data.duration / 2;export const yellowWalkWarnAt = 0; //states.Yellow().data.duration;
const sharedState = createMachine( sharedStates, { State: { change: (changes: Partial<CommonStateProps>) => (ev) => sharedStates.State({ ...ev.from.data, ...changes, }), }, }, sharedStates.State({ key: "State", crossingRequested: false }));
// Common transitions that all normal states can useconst commonTransitions = { emergency: "FlashingYellow", malfunction: "FlashingRed",} as const;
export const createExtendedTrafficLightMachine = () => { // Create the base machine with transitions const baseMachine = createMachine( states, { Green: { next: "Yellow", ...commonTransitions, }, Yellow: { next: "Red", ...commonTransitions, }, Red: { next: "Green", crossingRequested: "RedWithPedestrianRequest", ...commonTransitions, }, RedWithPedestrianRequest: { next: "Green", ...commonTransitions, }, FlashingYellow: { reset: "Red", }, FlashingRed: { reset: "Red", }, }, "Red" );
// Simple and clean with addEventApi const machine = Object.assign(addEventApi(baseMachine), { data: sharedState, requestCrossing: () => { sharedState.send("change", { crossingRequested: true }); machine.api.crossingRequested(); }, });
let timer: NodeJS.Timeout | null = null; let walkWarnTimer: NodeJS.Timeout | null = null;
setup(machine)( enter( whenState("Red", (_ev) => { const state = machine.data.getState(); if (state.data.crossingRequested) { queueMicrotask(machine.api.crossingRequested); } }) ), enter( whenState("RedWithPedestrianRequest", (_ev) => { machine.data.send("change", { crossingRequested: false, }); }) ), enter((ev) => { if (walkWarnTimer) { clearTimeout(walkWarnTimer); walkWarnTimer = null; }
// update pedestrian timer if (ev.to.is("Red")) { sharedState.send("change", { walkWarningDuration: 0, }); } else { const walkWarnAt = ev.to.is("Green") ? greenWalkWarnAt : ev.to.is("Yellow") ? yellowWalkWarnAt : -1; if (walkWarnAt > -1) { const walkWarningDuration = ev.to.is("Green") ? walkDuration - greenWalkWarnAt : ev.to.is("Yellow") ? states.Yellow().data.duration - yellowWalkWarnAt : 0; walkWarnTimer = setTimeout(() => { sharedState.send("change", { walkWarningDuration }); }, walkWarnAt); } }
// update timer for normal states (not flashing states) if (timer) { clearTimeout(timer); timer = null; }
const duration = ev.to.data.duration; if (duration === 0) { return; } timer = setTimeout(() => { machine.api.next(); }, duration); }) ); // start it machine.send("next"); return machine;};
export type ExtendedTrafficLightMachine = ReturnType< typeof createExtendedTrafficLightMachine>;
import { useCallback, useEffect } from "react";
export function useIntervalEffect(effect: () => void, ms: number | null) { const cb = useCallback(effect, []); useEffect(() => { if (ms === null) return; const interval = setInterval(cb, ms); return () => clearInterval(interval); }, [cb, ms]);}
import { useMachine } from "matchina/react";import { walkDuration, type ExtendedTrafficLightMachine } from "./machine";import { useEffect, useState } from "react";import { useIntervalEffect } from "./hooks";
export const ExtendedTrafficLightView = ({ machine,}: { machine: ExtendedTrafficLightMachine;}) => { useMachine(machine); const currentState = machine.getState(); const pedestrianSignal = currentState.data.pedestrian;
useMachine(machine.data); const data = machine.data.getState(); const walkWarningDuration = data.data.walkWarningDuration;
const [timeRemaining, setTimeRemaining] = useState( currentState.data.duration ); const [walkTimeRemaining, setWalkTimeRemaining] = useState(0); const [isBlinking, setIsBlinking] = useState(false); const [walkBlinking, setWalkBlinking] = useState(false);
const progressPercent = Math.max( 0, Math.min(100, (timeRemaining / currentState.data.duration) * 100) ); // Set initial walk warning duration useEffect(() => { if (walkWarningDuration) { setWalkTimeRemaining(walkWarningDuration); } else { setWalkTimeRemaining(0); } }, [walkWarningDuration]);
// Set initial time remaining when state changes useEffect(() => { setTimeRemaining(currentState.data.duration); }, [currentState]);
// Handle light countdown useIntervalEffect( () => setTimeRemaining((prev) => Math.max(0, prev - 100)), currentState.data.duration > 0 ? 100 : null );
// Handle walk countdown useIntervalEffect( () => setWalkTimeRemaining((prev) => Math.max(0, prev - 100)), walkTimeRemaining > 0 ? 100 : null );
// Handle traffic light blinking useIntervalEffect( () => setIsBlinking((prev) => !prev), currentState.is("FlashingYellow") || currentState.is("FlashingRed") ? 500 : null ); useIntervalEffect( () => { setWalkBlinking((prev) => !prev); }, walkTimeRemaining > 0 ? 500 : null );
return ( <div className="flex flex-col items-center"> <div className="flex space-x-8 mb-4 items-end"> {/* Traffic light */} <div className="bg-gray-800 p-4 rounded-lg"> <div className="flex flex-col space-y-4 items-center"> {/* Red light */} <div className={`w-16 h-16 rounded-full ${currentState.match( { Red: () => "bg-red-600", RedWithPedestrianRequest: () => "bg-red-600", FlashingRed: () => (isBlinking ? "bg-red-600" : "bg-red-900"), _: () => "bg-red-900", }, false )}`} /> {/* Yellow light */} <div className={`w-16 h-16 rounded-full ${currentState.match( { Yellow: () => "bg-yellow-400", FlashingYellow: () => isBlinking ? "bg-yellow-400" : "bg-yellow-900", _: () => "bg-yellow-900", }, false )}`} /> {/* Green light */} <div className={`w-16 h-16 rounded-full ${currentState.match( { Green: () => "bg-green-500", _: () => "bg-green-900", }, false )}`} /> </div> </div>
{/* Pedestrian signal */} <div className="bg-gray-800 p-4 rounded-lg"> <div className="flex flex-col items-center"> <div className="h-8 flex items-center justify-center"> {pedestrianSignal.match({ Walk: () => ( <span className="text-green-500 text-2xl" style={ walkTimeRemaining > 0 ? { opacity: walkBlinking ? 1 : 0.3 } : undefined } > 🚶 </span> ), DontWalk: () => ( <span className="text-red-500 text-2xl">✋</span> ), Error: () => ( <span className="text-yellow-500 text-2xl">⚠️</span> ), })} </div> <div className="text-sm text-center w-16 h-8 flex items-center justify-center"> {pedestrianSignal.match({ Walk: () => "WALK", DontWalk: () => "DON'T WALK", Error: () => "ERROR", })} </div> <div className="h-4 flex items-center justify-center"> {walkTimeRemaining > 0 ? ( <div className="text-yellow-600 font-mono text-center"> {`${(walkTimeRemaining / 1000).toFixed(1)}s`} </div> ) : ( <div className="invisible">0.0s</div> )} </div> </div> </div> </div>
{/* Light Countdown - only show for non-flashing states */} {!currentState.is("FlashingYellow") && !currentState.is("FlashingRed") && ( <> <div className="w-64 h-2 bg-gray-200 rounded-full mb-2"> <div className={`h-full rounded-full ${currentState.match( { Green: () => "bg-green-500", Yellow: () => "bg-yellow-400", Red: () => "bg-red-600", RedWithPedestrianRequest: () => "bg-red-600", }, false )}`} style={{ width: `${progressPercent}%` }} ></div> </div> <div className="text-xs text-gray-600 mb-2">Light Countdown</div> </> )}
{/* Walk Countdown */} {walkWarningDuration && walkTimeRemaining > 0 && ( <> <div className="w-64 h-2 bg-gray-200 rounded-full mb-2"> <div className={`h-full rounded-full bg-yellow-500`} style={{ width: `${(walkTimeRemaining / walkDuration) * 100}%` }} ></div> </div> <div className="text-xs text-gray-600 mb-2">Walk Countdown</div> </> )}
<div className="text-xl font-bold mb-2">{currentState.data.message}</div>
<div className="text-sm mb-4"> Current state: <span className="font-mono">{currentState.key}</span> </div>
<div className="flex space-x-4 mb-4"> {!currentState.is("FlashingYellow") && !currentState.is("FlashingRed") && ( <> <button className="px-4 py-2 rounded bg-blue-500 text-white hover:bg-blue-600" onClick={() => machine.api.next()} > Next Signal </button> {!data.data.crossingRequested ? ( <button className="px-4 py-2 rounded bg-green-500 text-white hover:bg-green-600" onClick={() => machine.requestCrossing()} > Request Crossing </button> ) : ( <button className="px-4 py-2 rounded bg-gray-500 text-white hover:bg-gray-600" disabled > Crossing Requested </button> )} </> )} </div>
{/* Special mode controls */} <div className="flex space-x-4"> {!currentState.is("FlashingYellow") && !currentState.is("FlashingRed") ? ( <> <button className="px-4 py-2 rounded bg-yellow-500 text-white hover:bg-yellow-600" onClick={() => machine.api.emergency()} > Emergency Mode </button> <button className="px-4 py-2 rounded bg-red-500 text-white hover:bg-red-600" onClick={() => machine.api.malfunction()} > Malfunction </button> </> ) : ( <button className="px-4 py-2 rounded bg-blue-500 text-white hover:bg-blue-600" onClick={() => machine.api.reset()} > Reset to Normal </button> )} </div> </div> );};
import { useMemo } from "react";import { ExtendedTrafficLightView } from "./TrafficLightView";import { createExtendedTrafficLightMachine } from "./machine";
export function ExtendedTrafficLightDemo() { const machine = useMemo(createExtendedTrafficLightMachine, []); return <ExtendedTrafficLightView machine={machine} />;}
How It Works
Section titled “How It Works”The traffic light normally cycles through Green → Yellow → Red → Green, but when a pedestrian presses the button:
- If the light is already Red, it transitions to RedWithPedestrian to allow crossing
- If the light is Green or Yellow, it continues its normal cycle until reaching Red
- From RedWithPedestrian, the light returns to Green after pedestrians have crossed
Additionally, the light can enter special states:
- Emergency Mode (Flashing Yellow): Indicates caution, proceed if safe
- Malfunction Mode (Flashing Red): Treat as a stop sign, stop and then proceed when safe
These special states demonstrate how state machines can model exceptional conditions and transitions between normal and exceptional operation.
This pattern demonstrates how state machines can handle external inputs differently based on their current state, providing a clean way to model complex interactive systems.
Next Steps
Section titled “Next Steps”Now that you’ve seen a more complex state machine example, you might want to explore:
- Nested States - Create hierarchical state machines
- Effect Handlers - Add side effects to your transitions
- History States - Remember previous states