statekit • Page 8 of 14

States

17/10/2026 • 3 min read

States are declared, typed names for what the machine is doing. If you keep them in data as isLoading and isFailed, nothing stops both being true. setState replaces the current state, so states can't disagree.

import { createMachine } from "@statekit/core"

const uploadMachine = createMachine({
   states: ["idle", "uploading", "done", "failed"],
   events: {
      start: ({ setState, S }) => setState(S.uploading),
      finish: ({ setState, S }) => setState(S.done),
      fail: ({ setState, S }) => setState(S.failed),
   },
})

const { STATE } = uploadMachine

// the first declared state starts active
uploadMachine.state === STATE.idle // true

uploadMachine.event.start()
uploadMachine.state === STATE.uploading // true

Inside an event, S is the machine's STATE and setState switches state. Outside, call machine.setState(machine.STATE.done). A misspelled state is a type error in both.

States are flags

Each state is one bit: with the states above, STATE is { idle: 1, uploading: 2, done: 4, failed: 8 }, and a machine has room for 32. setState replaces every active state with one, so === works while a machine is in one state at a time.

The bits let a machine be in several states at once. setState.enter turns one on and keeps the others, setState.exit turns one off:

const playerMachine = createMachine({
   states: ["idle", "playing", "buffering"],
   events: {
      play: ({ setState, S }) => setState(S.playing),
      bufferStart: ({ setState, S }) =>
         setState.enter(S.buffering),
      bufferEnd: ({ setState, S }) =>
         setState.exit(S.buffering),
      stop: ({ setState, S }) => setState(S.idle),
   },
   transitionRules: {
      deny: {
         idle: ["buffering"],
      },
   },
})

Play, then start buffering, and the machine is playing and buffering at once: state is 0b110, which is 6. The rule makes buffering from idle an error:

Loading demo…

With more than one state on, check a state with &:

const isBuffering =
   (playerMachine.state &
      playerMachine.STATE.buffering) !==
   0

Use that check on a machine you didn't write. A decoration can turn states on next to the active ones, and then === misses.

After an await

Inside an async event, check the state after an await with hasState. It reads the state at the time of the call. A state destructured from the first parameter is the state the event started with:

const uploadChanges = () =>
   new Promise((resolve) => setTimeout(resolve, 1000))

const syncMachine = createMachine({
   states: ["idle", "syncing", "paused"],
   events: {
      sync: async ({ setState, hasState, S }) => {
         setState(S.syncing)
         await uploadChanges()

         // paused while uploading? leave it paused
         if (hasState(S.paused)) return
         setState(S.idle)
      },
      pause: ({ setState, S }) => setState(S.paused),
   },
})

hasState(S.idle | S.paused) is true when either is active.

Transition rules

transitionRules limits where a state can go. For a named state, only lists the states it may move to and deny the states it may not:

const checkoutMachine = createMachine({
   states: ["cart", "paying", "paid", "failed"],
   events: {
      pay: ({ setState, S }) => setState(S.paying),
      succeed: ({ setState, S }) => setState(S.paid),
      fail: ({ setState, S }) => setState(S.failed),
   },
   transitionRules: {
      only: {
         paying: ["paid", "failed"],
      },
      deny: {
         cart: ["paid"],
      },
   },
})

A blocked transition throws an error that starts Cannot make transition: cart (1) => paid (100), with the states as bits. With several states active, every active state's rule must allow the one being entered. Leaving a state is always allowed. A state in both only and deny throws when the machine is created.

Data or states?

A state is what the machine is doing, the box you'd draw in a diagram. Data is what it knows. An upload's progress percentage is data, while uploading is a state.

#javascript, #npm, #react, #readme, #typescript, #web-development

Loading comments...