Stopwatch with React State and Effects
Interactive Stopwatch Example
Section titled “Interactive Stopwatch Example”About This Example
Section titled “About This Example”This example demonstrates a React-first implementation using a declarative state machine:
- Uses React’s native state management and effects
- Encapsulates all state machine logic in a custom hook
- Simple, idiomatic React code with clear patterns
The useStopwatch
hook relies on React for both its state management and effects, making it familiar to React developers.
Machine Code
Section titled “Machine Code”import { defineStates, createMachine, assignEventApi } from "matchina";import { useMachine } from "matchina/react";import { useState, useMemo, useEffect } from "react";import { tickEffect } from "../lib/tick-effect";
export function useStopwatch() { const [elapsed, setElapsed] = useState(0); const effects = useMemo( () => ({ run: () => { let lastTick = Date.now(); return tickEffect(() => { const now = Date.now(); setElapsed(stopwatch.elapsed + now - lastTick); lastTick = now; }); }, clear: () => { setElapsed(0); }, }), [] );
// Define the state machine const stopwatch = useMemo(() => { // Define states using defineStates const states = defineStates({ Stopped: {}, Ticking: {}, Suspended: {}, });
// Create the base machine with states, transitions, and initial state const baseMachine = createMachine( states, { Stopped: { start: "Ticking", }, Ticking: { stop: "Stopped", suspend: "Suspended", clear: "Ticking", }, Suspended: { stop: "Stopped", resume: "Ticking", clear: "Suspended", }, }, "Stopped" );
//Use assignEventApi to enhance the machine with utility methods return Object.assign(assignEventApi(baseMachine), { elapsed: elapsed, setElapsed: setElapsed, }); }, []);
stopwatch.elapsed = elapsed; useMachine(stopwatch); useEffect(() => { // if (stopwatch.changeProperty.type === "clear") { // effects.clear(); // } return stopwatch.getState().match( { Ticking: effects.run, Stopped: () => effects.clear, }, false ); }, [stopwatch.getState()]); return stopwatch;}
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 { MachineExampleWithChart } from "@components/MachineExampleWithChart";import { StopwatchView } from "./StopwatchView";import { useStopwatch } from "./useStopwatch";
// Main export for importing in MDX documentationexport default function StopwatchExample() { const stopwatch = useStopwatch(); return ( <MachineExampleWithChart machine={stopwatch} AppView={StopwatchView} showRawState={true} /> );}
export function Stopwatch() { const stopwatch = useStopwatch(); return <StopwatchView machine={stopwatch as any} />;}