Stopwatch State Machine
Interactive Stopwatch Example
Section titled “Interactive Stopwatch Example”Introduction
Section titled “Introduction”This example demonstrates a stopwatch state machine with start, stop, and reset functionality. It shows how to manage time-based state and lifecycle hooks for continuous updates.
State Definition
Section titled “State Definition”The stopwatch has three states:
- Stopped: The initial state with an elapsed time of 0
- Ticking: The running state that updates elapsed time
- Suspended: A paused state that preserves elapsed time
The machine uses lifecycle hooks to implement the timer functionality, automatically updating the elapsed time when in the Ticking state.
Machine Code
Section titled “Machine Code”import { defineStates, createMachine, setup, enter, when, effect, assignEventApi, before,} from "matchina";import { tickEffect } from "../lib/tick-effect";
export const createStopwatchMachine = () => { // Define states using defineStates const states = defineStates({ Stopped: (elapsed = 0) => ({ elapsed }), Ticking: (elapsed = 0) => ({ elapsed, at: Date.now() }), Suspended: (elapsed = 0) => ({ elapsed }), });
// Create the base machine with states, transitions, and initial state const baseMachine = createMachine( states, { Stopped: { start: "Ticking" }, Ticking: { _tick: "Ticking", stop: "Stopped", suspend: "Suspended", clear: "Ticking", }, Suspended: { stop: "Stopped", resume: "Ticking", clear: "Suspended", }, }, states.Stopped() );
//Use assignEventApi to enhance the machine with utility methods const machine = Object.assign(assignEventApi(baseMachine), { elapsed: 0, });
// Setup hooks directly on the machine (no need for .machine with assignEventApi) setup(machine)( // Before transition handler before((ev) => { if (ev.type === "_tick" && ev.from.is("Ticking")) { ev.to.data.elapsed = ev.from.data.elapsed + (Date.now() - ev.from.data.at); } if (ev.type === "clear") { ev.to.data.elapsed = 0; } return () => {}; }), // Effect when entering Ticking state enter( when( (ev) => ev.to.is("Ticking"), () => tickEffect(machine._tick) ) ), // Effect for all transitions - update elapsed field effect((ev) => { machine.elapsed = ev?.to.data.elapsed ?? 0; }) );
return machine;};
// Basic StopwatchView component for the exampleimport { getAvailableActions as getStateEvents } from "matchina";import { useMachine } from "matchina/react";
/** * A simple Stopwatch UI component that displays the current state, elapsed time, * and available actions based on the current state. */export function StopwatchView({ machine }: { machine: any }) { useMachine(machine); const state = machine.getState(); console.log("Current state:", state.key, machine.elapsed, machine); // Generate color class based on state const stateColorClass = state.match({ Stopped: () => "text-red-500", Ticking: () => "text-green-500", Suspended: () => "text-yellow-500", });
return ( <div className="p-4 rounded border"> {/* State display */} <div className={`inline ${stateColorClass}`}>{state.key}</div>
{/* Elapsed time */} <div className="text-4xl font-bold my-4"> {(machine.elapsed / 1000).toFixed(1)}s </div>
{/* Action buttons */} <div className="flex flex-wrap items-center gap-2"> {getStateEvents(machine.transitions, state.key) .filter((event) => !event.startsWith("_")) .map((event) => ( <button className="px-3 py-1 rounded bg-blue-500 text-white text-sm" key={event} onClick={() => { (machine as any)[event](); }} > {event} </button> ))} </div> </div> );}
import { createStopwatchMachine } from "./machine";import { StopwatchView } from "./StopwatchView";
export function Stopwatch() { const machine = createStopwatchMachine(); return <StopwatchView machine={machine} />;}
// Default export for the examplesexport default Stopwatch;
export function tickEffect(tick: () => void, interval = 50) { const timer = setInterval(tick, interval); return () => clearInterval(timer);}