Introduction

What is 1state

A small finite state machine for JavaScript, shaped after SCXML. No dependencies. ESM only.

It exists for multi-step forms: flows where the next question depends on the answers already given, where the user leaves halfway through, and where the flow gains a step while they are away.

Three packages:

PackageWhat
@1state/corethe engine
@1state/validatestatic checks for a definition, plus a CLI
@1state/mermaidrender a definition as a diagram, plus a CLI

Node 20+.

A quick example

import { createMachine, assign } from '@1state/core'

const signup = {
  context: { email: null, password: null },
  initial: 'email',
  states: {
    email: {
      on: {
        answer: { target: 'email', actions: assign({ email: (_ctx, e) => e.value }) },
      },
      go: [{ target: 'password', cond: (ctx) => ctx.email !== null }],
    },
    password: {
      on: {
        answer: { target: 'password', actions: assign({ password: (_ctx, e) => e.value }) },
      },
      go: [{ target: 'done', cond: (ctx) => ctx.password !== null }],
    },
    done: { type: 'final' },
  },
}

const machine = createMachine(signup)

await machine.start() // { value: "email", context: {...}, done: false }
await machine.send('answer', { value: 'a@example.org' }) // value: "password"
await machine.path() // ["email", "password"]
await machine.send('answer', { value: 'hunter2' }) // done: true

Two things carry the model

on records an answer as a self-transition: the state exits and re-enters, so its guards run again.

go is eventless: after every step the machine retries the go transitions of whatever is active, and keeps going until nothing opens. Where it stops is where the user is.

Everything else on the site follows from those two, and from the property they produce.

Position is derived, never stored

Nothing anywhere records “the user is on step 3”. The position is a pure function of the answers, recomputed from the initial state on every start(). Guards read the data.

const resumed = createMachine(signup, { context: { email: 'a@example.org' } })
await resumed.start() // value: "password"

No cursor was loaded. That one property buys resume, no skip-ahead and forward migration — three features that are otherwise three implementations. See Derived position.

Next steps

  • Quick Start — install and write a flow.
  • Derived position — the central idea, and the rule guards have to follow to keep it.
  • Composition — sections, onFinal, and two entry points from one definition.
  • URLs — the node id is the path, and route() is the whole page contract.
  • Limits — what this does not do.