🤖 Robot

Fast 1kB functional library for creating Finite State Machines

One of the goals of Robot is to make Finite State Machines more composable. In other implementations you create a machine by constructing a large options object with all of the machine's states, transitions, etc. This makes it a bit harder to reuse useful parts of the machine.

When using Robot you'll notice how easy it is to break up reusable parts. This is thanks to its composition over configuration nature. For example, take this full name machine:

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

const machine = createMachine(
form: state(
transition('first', 'form',
reduce((ctx, ev) => ({ ...ctx, first: ev.event.target.value }))
),
transition('last', 'form',
reduce((ctx, ev) => ({ ...ctx, last: ev.event.target.value }))
)
)
);

This, and many form fields like it, follow a similar pattern:

  1. Listen for an event that is named the same as the property, first and last in this case.
  2. Run a reducer to save the value to the machine context.
  3. Return to the original state, form, to wait for the next event.

We can improve this by creating a generic field function like so:

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

const field = (prop, state) => (
transition(prop, state,
reduce((ctx, ev) => ({ ...ctx, [prop]: ev.event.target.value }))
)
);

const formField = (prop) => field(prop, 'form');

const machine = createMachine(
form: state(
formField('first'),
formField('last')
)
);

Since this is such a common pattern you might want to move these types of reusable transitions to their own module:

transitions.js

export const field = (prop, state) => (
transition(prop, state,
reduce((ctx, ev) => ({ ...ctx, [prop]: ev.event.target.value }))
)
);

export const formField = (prop) => field(prop, 'form');

Making your machine codes much more succinct:

import { createMachine, state } from 'robot3';
import { formField } from './transitions.js';

const machine = createMachine(
form: state(
formField('first'),
formField('last')
)
);

You might find that you need a slight modification to how one of the transitions works. For example, we want to put a limit to first at only 10 characters. We can change our field transition to take additional arguments:

export const field = (prop, state, ...args) => (
transition(prop, state,
reduce((ctx, ev) => ({ ...ctx, [prop]: ev.event.target.value })),
...args
)
);

export const formField = (prop, ...args) => field(prop, 'form', ...args);

And use a guard in our usage:

import { createMachine, guard, state } from 'robot3';
import { formField } from './transitions.js';

const machine = createMachine(
form: state(
formField('first',
guard((ctx) => ctx.first.length <= 10)
),
formField('last')
)
);

I'm hopeful common useable patterns like the above will make their way to npm and an ecosystem emerges.