Black Lives Matter. Support the Equal Justice Initiative.

Robot

Fast 1kB functional library for creating Finite State Machines

# Robot

With Robot you can build finite state machines in a simple and flexible way.

import { createMachine, state, transition } from 'robot3';

const machine = createMachine({
inactive: state(
transition('toggle', 'active')
),
active: state(
transition('toggle', 'inactive')
)
});

export default machine;

Which you can use easily with any of our integrations:

import { h } from 'preact';
import { useMachine } from 'preact-robot';
import machine from './machine.js';

function Counter() {
const [current, send] = useMachine(machine);
const state = current.name;

return (
<>
<div>State: {state}</div>
<button onClick={() => send('toggle')}>
Toggle
</button>
</>
);
}

Robot emphasizes:

# Getting Started

Install the robot3 package via npm or Yarn:

npm install robot3
yarn add robot3

This demo shows how to use a Robot to create a loading state machine, including using the state to declaratively render different UI based on the state of the machine.

import { createMachine, invoke, reduce, state, transition } from 'robot3';
import { useMachine } from 'preact-robot';
import { h, render } from 'preact';

const context = () => ({
users: []
});

async function loadUsers() {
return [
{ id: 1, name: 'Wilbur' },
{ id: 2, name: 'Matthew' },
{ id: 3, name: 'Anne' }
];
}

const machine = createMachine({
idle: state(
transition('fetch', 'loading')
),
loading: invoke(loadUsers,
transition('done', 'loaded',
reduce((ctx, ev) => ({ ...ctx, users: ev.data }))
)
),
loaded: state()
}, context);

function App() {
const [current, send] = useMachine(machine);
const state = current.name;
const { users } = current.context;
const disableButton = state === 'loading' || state === 'loaded';

return (
<>
{state === 'loading' ? (
<div>Loading users...</div>
) : state === 'loaded' ? (

<ul>
{users.map(user => {
<li id={`user-${user.id}`}>{user.name}</li>
})}
</ul>

): ()}

<button onClick={() => send('fetch')} disabled={disableButton}>
Load users
</button>
</>
)
}

render(<App />, document.getElementById('app'));

# Why Finite State Machines

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:

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:

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:

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:

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:

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:

# Inspiration

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