Skip to content

Stopwatch with React state and useLifecycle

Stopped
0s
Loading...
{
  "type": "__initialize",
  "to": {
    "data": {},
    "key": "Stopped"
  }
}

This version uses onLifecycle instead of useEffect.

It is somewhat verbose, and mainly serves to show how the onLifecycle hook could be used here.

One nice thing about onLifecycle is the autocomplete experience for the developer that makes it easy to explore the available extensions for each state and event.

And within a given state or event config, the event will be constrained to that state and/or event. For example { Rejected: { enter: ev => { ev.data.error } } } would autocomplete ev.error that is part of the Rejected state data.

Full Code

63 lines, 1831 chars
import React, { useEffect, useMemo, useState } from "react";
import { onLifecycle, when } from "matchina";

import { useMachine } from "matchina/integrations/react";
import { StopwatchDevView, tickEffect } from "./StopwatchCommon";
import { matchina } from "matchina/dev/matchina";

function useStopwatch() {
  const [startTime, setStartTime] = useState<number | undefined>(undefined);  
  const [elapsed, setElapsed] = useState(0); 
  // Define the state machine
  const stopwatch = useMemo(() => Object.assign(matchina({
      Stopped: {}, 
      Ticking: {}, 
      Suspended: {},
    }, {
      Stopped: {
        start: 'Ticking'
      },
      Ticking: {
        stop: 'Stopped',
        suspend: 'Suspended',
        clear: 'Ticking'
      },
      Suspended: {
        stop: 'Stopped',
        resume: 'Ticking',
        clear: 'Suspended'
      }
    }, 'Stopped'), { 
      startTime,
      elapsed, 
    }), [])
  useEffect(() => onLifecycle(stopwatch.machine, {
    '*': {
      enter: when(ev => ev.to.is('Ticking'), () => tickEffect(() => {
        setElapsed(Date.now() - (stopwatch.startTime ?? 0));
      })),
      on: { 
        start: { effect: () => { setStartTime(Date.now()) } },
        clear: { effect: () => setElapsed(0) },
        stop: { effect: () => setElapsed(0) },
        resume: { effect: () => setStartTime(Date.now() - (stopwatch.elapsed??0)) }
      }
    },
    Ticking: {
      on: { clear: { effect() { setStartTime(Date.now()) } } },        
    },
    Suspended: {
      on: { clear: { effect () { setStartTime(undefined) } } }
    },
  }), [stopwatch])
  useMachine(stopwatch.machine)
  stopwatch.startTime = startTime
  stopwatch.elapsed = elapsed
  return stopwatch
}

export function Stopwatch () {
  const stopwatch = useStopwatch()
  return <StopwatchDevView stopwatch={stopwatch} />  
}