Stopwatch with Data and Transition Functions
Interactive Stopwatch Example
Section titled “Interactive Stopwatch Example”About This Example
Section titled “About This Example”This example demonstrates a function-oriented approach to state machine implementation. It showcases:
- Using state
data
for trackingelapsed
time without React state - Function-based state transitions for rich TypeScript type inference
- Separation of machine logic and UI components
This approach is more verbose but provides stronger typing and clearer data flow between states.
Machine Code
Section titled “Machine Code”import { defineStates, createMachine, setup, enter, when, effect, assignEventApi,} from "matchina";import { tickEffect } from "../lib/tick-effect";
export const createStopwatchMachine = () => { // Define states using defineStates const states = defineStates({ Stopped: () => ({ elapsed: 0 }), 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: () => (ev) => states.Ticking( !ev ? 0 : ev?.from.data.elapsed + (Date.now() - ev?.from.data.at) ), stop: "Stopped", suspend: () => (ev) => states.Suspended(ev?.from.data.elapsed), clear: "Ticking", }, Suspended: { stop: "Stopped", resume: () => (ev) => states.Ticking(ev?.from.data.elapsed), clear: "Suspended", }, }, "Stopped" );
//Use assignEventApi to enhance the machine with utility methods const machine = Object.assign(assignEventApi(baseMachine), { elapsed: 0, }); setup(machine)( enter( when( (ev) => ev.to.is("Ticking"), () => tickEffect(machine._tick) ) ), effect((ev) => { machine.elapsed = ev.to.data.elapsed ?? 0; }) ); return machine;};
import { getAvailableActions as getStateEvents } from "matchina";import type { Stopwatch } from "../stopwatch-common/types";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: Stopwatch }) { useMachine(machine); const state = machine.getState();
// 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 { useMemo } from "react";import { StopwatchView } from "./StopwatchView";import { createStopwatchMachine } from "./machine";
export function Stopwatch() { const stopwatch = useMemo(createStopwatchMachine, []); return <StopwatchView machine={stopwatch as any} />;}
export function tickEffect(tick: () => void, interval = 50) { const timer = setInterval(tick, interval); return () => clearInterval(timer);}