beckingham.dev

Intro

I love Hacktober! For as many years as I’ve been developing, I’ve participated exactly… Zero times. Well, this year that’s about to change! I’ve been on a Svelte kick recently and was lamenting with a colleague about how frontend frameworks tend to be a bit overboard on cognitive complexity, and even Svelte with all it’s beautiful simplicity is often encumbered with complicated frameworks like XState.

He started getting rowdy about state machines and what purpose they actually serve. To quote:

it’s just a property though right, new, finished, submitted. A switch/case could do the same thing, no?

Honestly I didn’t have a good answer. I’ve been writing concurrent code for years that leverages Finite State Machine concepts in one way or another, and I had nothing to say. Upon reflection and a bit of searching, this blog post from OpenReplay explains the benefits of XState and FSM’s in general.

As I was searching, I found myself frantically hacking around the CodeSandbox examples of XState with various frameworks, and I came across this example page in the XState docs. At the time of writing this blog post there is a React sample and a Vue sample.

Immediately, I knew that my budding Svelte evangelism would not rest until I at least attempted to create a Svelte equivalent. Not only that, but basic calculators appear to be a really good demonstration of Finite State Machine basics (Ask me again when I’ve finished if I still think that though…)

I clicked through to the demo to see how it ran and although it ran fine performance and output wise, I was taken aback to find both examples implemented some very common calculator behaviours incorrectly. I’m not sure if there is an international standardisation of calculators, but when I use a basic calculator I expect the following behaviours:

  1. If you press 0, nothing happens unless another number has already been pressed. Both demos would take repeated 0 and add leading zeroes.
  2. If you press = n times, it will repeat the last operation n times. i.e. 2 x 2 = would display 4, pressing = again would display 8, and again would display 16, then 32, 64, 128 and so on until buffer overflow. Neither demo did this, meaning you would have to type x 2 = every time.

I was already determined to write the Svelte demo as I was playing around, so to keep myself as naive as possible I avoided looking into the code. I’m competent with Svelte, but a complete noob at XState. What you’re about to witness is me gradually building a calculator state machine and demonstrating how FSM’s provide robust security around managing state.

Part 0: Setting up the bits

First port of call, setting up the project.

  1. Create a new repo
  2. Install Svelte
  3. Install XState

Following all the instructions and a quick pnpm run dev later we should see the beautiful Svelte “Hello World” page, letting us know we’re ready to rock 🤘.

Part 1: Defining initial state

The start seems like a good place to… start. When we look at a calculator, what is the initial state? We see a display with the digit 0 on it, and that’s about it! We can simply call this state Initial for now, but you may be wondering where the 0 on the display factors into it. Indeed, why not call this state Display 0? Well, we’re talking about Finite State Machines here, and if we factored the number on the display into the state itself then it wouldn’t be very finite at all!

In XState, data used by the machine is stored in a context option and this is where that display 0 value will live for now. Here is the code so far:

import { createMachine } from "xstate";

type CalculatorEvent = undefined

type CalculatorTypeState = 
    | { value: 'initial', context: CalculatorContext }

interface CalculatorContext {
    display: number
}

export const calculatorMachine = createMachine<CalculatorContext, CalculatorEvent, CalculatorTypeState>({
  key: 'calculator',
  initial: 'initial',
  context: {
    display: 0,
  },
  states: {
      initial: {}
  }
})
<script lang="ts">
	import { interpret } from "xstate";
	import { calculatorMachine } from "./calculatorMachine";

	let calculator;

	const calcMachine = interpret(calculatorMachine)
		.onTransition((state) => (calculator = state))
		.start();
</script>

<main>
	<h1>Calculator state is {calculator.value}</h1>
</main>

<style>
	main {
		text-align: center;
		padding: 1em;
		max-width: 240px;
		margin: 0 auto;
	}
</style>

So if that’s all gone to plan, you should be looking at something that looks like this:

Calculator state is initial

And then draw the rest of the fucking owl, you should get:

Calculator state is initial

0