Stopwatch with External React State
Interactive Stopwatch Example
Section titled “Interactive Stopwatch Example”About This Example
Section titled “About This Example”This example demonstrates a state machine with external React state management:
- Uses React’s
useState
to manage the elapsed time outside the machine - Connects the external state to the machine via props
- Implements effects as functions that can access and modify the external state
- Uses utility hooks for state and event effects
This pattern is particularly useful when you need to:
- Integrate with existing React state management
- Share state across multiple components
- Keep state management separate from the state machine logic
Machine Code
Section titled “Machine Code”import { createMachine, defineStates, assignEventApi } from "matchina";import { tickEffect } from "../lib/tick-effect";
export const createStopwatchMachine = ( elapsed: number, setElapsed: (elapsed: number) => void) => { const effects = { clear: () => setElapsed(0), run: () => { let lastTick = Date.now(); return tickEffect(() => { const now = Date.now(); setElapsed(stopwatch.elapsed + now - lastTick); lastTick = now; }); }, };
const states = defineStates({ Stopped: { effects: [effects.clear] }, Ticking: { effects: [effects.run] }, Suspended: {}, });
const stopwatch = Object.assign( assignEventApi( createMachine( states, { Stopped: { start: "Ticking", }, Ticking: { stop: "Stopped", suspend: "Suspended", clear: "Ticking", }, Suspended: { stop: "Stopped", resume: "Ticking", clear: "Suspended", }, }, "Stopped" ) ), { elapsed, effects, } );
return stopwatch;};
import { useMachine } from "matchina/react";import { useEffect, useState } from "react";import { useEventTypeEffect, useStateEffects } from "../lib/matchina-hooks";import { createStopwatchMachine } from "./machine";
export const useStopwatch = () => { const [elapsed, setElapsed] = useState(0); const [stopwatch] = useState(() => createStopwatchMachine(elapsed, setElapsed) );
useEffect(() => { stopwatch.elapsed = elapsed; }, [elapsed]);
useMachine(stopwatch); const state = stopwatch.getState(); const lastChange = stopwatch.getChange(); useStateEffects(state); useEventTypeEffect(lastChange, stopwatch.effects); 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); // console.log("StopwatchView", 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 { StopwatchView } from "./StopwatchView";import { useStopwatch } from "./useStopwatch";
export function Stopwatch() { const stopwatch = useStopwatch(); return <StopwatchView machine={stopwatch as any} />;}