ROBOT

1KB OF PURE MECHANICAL MAGIC

TINY_FOOTPRINT.exe

Just 1KB of pure robotic efficiency. No bloat, all bot!

TYPE_SAFE.sys

Bulletproof typing that would make even a robot proud

DOCS.readme

Documentation so clear, even humans can understand it

Why Finite State Machines

A better way to handle complex UI state

With Finite State Machines the term state might not mean what you think. In the frontend we tend to think of state to mean all of the variables that control the UI. When we say state in Finite State Machines, we mean a higher-level sort of state.

For example, on the GitHub issue page, the issue titles can be edited by the issue creator and repo maintainers. Initially a title is displayed like this:

Issue title in preview mode

The edit button (in red) changes the view so that the title is in an input for editing, and the buttons change as well:

Issue title in edit mode

If we call this edit mode you might be inclined to represent this state as a boolean and the title as a string:

SIMPLE_STATE.js
let editMode = false;
let title = '';

When the Edit button is clicked you would toggle the editMode variable to true. When Save or Cancel are clicked, toggle it back to false.

But oops, we're missing something here. When you click Save it should keep the changed title and save that via an API call. When you click Cancel it should forget your changes and restore the previous title.

So we have some new states we've discovered, the cancel state and the save state. You might not think of these as states, but rather just some code that you run on events. Think of what happens when you click Save; it makes an external request to a server. That request could fail for a number of reasons. Or you might want to display a loading indicator while the save is taking place. This is definitely a state! Cancel, while more simple and immediate, is also a state, as it at least requires some logic to tell the application that the newly inputted title can be ignored.

You can imagine this component having more states as well. What should happen if the user blanks out the input and then clicks save? You can't have an empty title. It seems that this component should have some sort of validation state as well. So we've identified at least 6 states:

  • preview: The default view when on an issue page.
  • edit: When in edit mode.
  • save: When saving to a remote API.
  • error: When the API server errors for some reason.
  • cancel: When rolling back changes from edit mode.
  • validate: When confirming the new input title is an acceptable string.

I'll spare you the code, but you can imagine that writing this logic imperatively can result in a number of bugs. You might be tempted to represent these states as a bunch of booleans:

BOOLEAN_FLAGS.js
let editMode = false;
let saving = false;
let validating = false;
let saveHadError = false;

And then toggle these booleans in response to the appropriate event. We've all written code this way. You can pull it off, but why do so when you don't have to? Take, for example, what happens when new requirements are added, resulting in yet another new state of this component. You would need to add another boolean, and change all of your code to toggle the boolean as needed.

In recent years there has been a revolution in declarative programming in the front-end. We use tools such as React to represent our UI as a function of state. This is great, but we still write imperative code to manage our state like this:

IMPERATIVE_STATE.js
function resetState() {
  setValidating(false);
  setSaving(false);
  setBlurred(false);
  setEditing(false);
  if(!focused) setTouched(false);
  setDirty(true);
}

Finite State Machines bring the declarative revolution to application (and component) state. By representing your states declaratively you can eliminate invalid states and prevent an entire category of bugs. Finite State Machines are like static typing for your states.

Robot is a Finite State Machine library meant to be simple, functional, and fun. With Robot you might represent this title component like so:

TITLE_MACHINE.robot
import { createMachine, guard, immediate, invoke, state, transition, reduce } from 'robot3';

const machine = createMachine({
  preview: state(
    transition('edit', 'editMode',
      // Save the current title as oldTitle so we can reset later.
      reduce(ctx => ({ ...ctx, oldTitle: ctx.title }))
    )
  ),
  editMode: state(
    transition('input', 'editMode',
      reduce((ctx, ev) => ({ ...ctx, title: ev.target.value }))
    ),
    transition('cancel', 'cancel'),
    transition('save', 'validate')
  ),
  cancel: state(
    immediate('preview',
      // Reset the title back to oldTitle
      reduce(ctx => ({ ...ctx, title: ctx.oldTitle }))
    )
  ),
  validate: state(
    // Check if the title is valid. If so go
    // to the save state, otherwise go back to editMode
    immediate('save', guard(titleIsValid)),
    immediate('editMode')
  ),
  save: invoke(saveTitle,
    transition('done', 'preview'),
    transition('error', 'error')
  ),
  error: state(
    // Should we provide a retry or...?
  )
});

This might seem like a lot of code, but consider that:

  • This prevents the component from ever being in an invalid state.
  • This captures all of the possible states. When writing imperative code you often ignore uncommon or inconvenient states like errors. With Finite State Machines it's much harder to ignore reality.
  • States and transitions are validated at the moment that the machine is created and will throw when using robot/debug.

Inspiration

Standing on the shoulders of giants

Robot is inspired by a variety of projects involving finite state machines including:

Statecharts

The specification that fixes some of the issues with Finite State Machines. Robot adopts a number of these changes.

XState

The excellent JavaScript library that implements the Statecharts XML spec.

The P Programming Language

A DSL for representing Finite State Machines.