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
useMemo
only to create the state machine - Avoids
useEffect
anduseState
dependencies - Makes
elapsed
a part of the state’sdata
- Uses lifecycle hooks to manage state transitions
- Automatically resets
elapsed
to0
when 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);}