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
useStateEffects
anduseEventTypeEffect
for 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 (like clear
), 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 by defineEffects
).
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";
export function useStopwatch() { const [elapsed, setElapsed] = useState(0);
const effects = useMemo( () => ({ clear: () => setElapsed(0), run: () => { let lastTick = Date.now(); return tickEffect(() => { const now = Date.now(); setElapsed(stopwatch.elapsed + now - lastTick); lastTick = now; }); }, }), [] );
// Define the state machine const stopwatch = useMemo(() => { // Define states using defineStates const states = defineStates({ Stopped: { effects: [effects.clear] }, Ticking: { effects: [effects.run] }, 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, }); }, []);
useMachine(stopwatch); useStateEffects(stopwatch.getState()); useEventTypeEffect(stopwatch.getChange(), effects); stopwatch.elapsed = elapsed; 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 { StopwatchView } from "./StopwatchView";import { useStopwatch } from "./useStopwatch";
export function Stopwatch() { const stopwatch = useStopwatch(); return <StopwatchView machine={stopwatch as any} />;}