Stopwatch using State Data and Hooks
Interactive Stopwatch Example
Section titled “Interactive Stopwatch Example”About This Example
Section titled “About This Example”This example demonstrates how to use state data instead of React’s useState. Key features:
- Uses
useMemoonly to create the state machine - Avoids
useEffectanduseStatedependencies - Makes
elapseda part of the state’sdata - Uses lifecycle hooks to manage state transitions
- Automatically resets
elapsedto0when stopped
The when helper functions as an alternative to useEffect, tracking when a condition is entered and running entry/exit handlers.
Machine Code
Section titled “Machine Code”import { defineStates, createMachine, setup, before, effect, when, assignEventApi,} from "matchina";import { tickEffect } from "../lib/tick-effect";
export const createStopwatchMachine = () => { // Define states using defineStates const states = defineStates({ Stopped: () => ({ elapsed: 0 }), Ticking: (elapsed: number = 0) => ({ at: Date.now(), elapsed }), 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", }, }, "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((ev) => { ev.to.data.elapsed = ev.match( { stop: () => 0, clear: () => 0, _: () => ev.from.data.elapsed, _tick: () => ev.from.data.elapsed + (Date.now() - ev.from.as("Ticking").data.at), }, false ); }), effect( when( (ev) => ev.to.is("Ticking"), () => tickEffect(machine._tick) ) ), effect((ev) => { machine.elapsed = ev.to.data.elapsed; }) ); return machine;};import { getAvailableActions as getStateEvents } from "matchina";import type { Stopwatch } from "../stopwatch-common/types";
/** * 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 }) { 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);}