Stopwatch with React State and State Effects
Interactive Stopwatch Example
Section titled “Interactive Stopwatch Example”About This Example
Section titled “About This Example”This is the cleanest, shortest version of the stopwatch examples, using specialized utility hooks:
- Uses
useStateEffectsanduseEventTypeEffectfor simplified effect management - Relies on React for state management like the React-first example
- Provides a more declarative approach to handling state-specific effects
The key innovation here is using effects tied directly to states and events (likeclear), which creates a more maintainable separation of concerns.
While this approach is elegant, a more idiomatic pattern would fully separate the machine definition from React into separate files, with effects specified as non-function JS values (strings or objects created bydefineEffects).
Machine Code
Section titled “Machine Code”import{createMachine,defineStates,assignEventApi}from"matchina";import{useMachine}from"matchina/react";import{useMemo,useState}from"react";import{useEventTypeEffect,useStateEffects}from"../lib/matchina-hooks";import{tickEffect}from"../lib/tick-effect";exportfunctionuseStopwatch(){const[elapsed,setElapsed]=useState(0);consteffects=useMemo(()=> ({clear:()=>setElapsed(0),run:()=>{letlastTick=Date.now();returntickEffect(()=>{constnow=Date.now();setElapsed(stopwatch.elapsed+now-lastTick);lastTick=now;});},}),[]);// Define the state machineconststopwatch=useMemo(()=>{// Define states using defineStatesconststates=defineStates({Stopped:{ effects: [effects.clear]},Ticking:{ effects: [effects.run]},Suspended:{},});// Create the base machine with states, transitions, and initial stateconstbaseMachine=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 methodsreturnObject.assign(assignEventApi(baseMachine),{elapsed,});}, []);useMachine(stopwatch);useStateEffects(stopwatch.getState());useEventTypeEffect(stopwatch.getChange(),effects);stopwatch.elapsed=elapsed;returnstopwatch;}import{getAvailableActionsasgetStateEvents}from"matchina";importtype{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.*/exportfunctionStopwatchView({machine}:{machine:Stopwatch}){useMachine(machine);conststate=machine.getState();// Generate color class based on stateconststateColorClass=state.match({Stopped:()=>"text-red-500",Ticking:()=>"text-green-500",Suspended:()=>"text-yellow-500",});return (<divclassName="p-4 rounded border">{/* State display */}<divclassName={`inline${stateColorClass}`}>{state.key}</div>{/* Elapsed time */}<divclassName="text-4xl font-bold my-4">{(machine.elapsed/1000).toFixed(1)}s</div>{/* Action buttons */}<divclassName="flex flex-wrap items-center gap-2">{getStateEvents(machine.transitions, state.key).filter((event)=>!event.startsWith("_")).map((event)=> (<buttonclassName="px-3 py-1 rounded bg-blue-500 text-white text-sm"key={event}onClick={()=>{(machineasany)[event]();}}>{event}</button>))}</div></div>);}import{StopwatchView}from"./StopwatchView";import{useStopwatch}from"./useStopwatch";exportfunctionStopwatch(){conststopwatch=useStopwatch();return<StopwatchViewmachine={stopwatchasany} />;}