Skip to content

Hierarchical state machines

Some state is not a number in app-db — it is which named stage you are in, and which events may legally leave it. Login is idle, then submitting, then authed or error or locked-out. A websocket is connecting, open, reconnecting, closed. Those flows usually hide as enums and booleans scattered across handlers.

A machine makes that shape explicit: one data table of states and transitions, driven by ordinary dispatch, read by ordinary subscribe, living in the same frame as the rest of re-frame2. No second runtime.

Prerequisites. The Core introduction — events, app-db, subscriptions, effects. Machines plug into those; they do not replace them.

A machine in four lines

(:require [re-frame.core :as rf]
          [re-frame.machines])   ;; opt-in artefact

(rf/defmachine door
  {:initial :closed
   :states  {:closed {:on {:open :open}}
             :open   {:on {:close :closed}}}})

(rf/reg-machine :door door)

(rf/dispatch [:door [:open]])
@(rf/subscribe [:rf/machine :door])
;; => {:state :open :data {}}

reg-machine is sugar over reg-event. The snapshot rides runtime-db — undo, Xray, SSR, and tests share the same pipeline as the rest of the app.

When not to use a machine

Situation Prefer
A counter, list, or form field app-db + events
Two stages (:loading? boolean) a keyword or flag
Server fetch / cache / invalidate resources
A fixed sequence of operations chained events / effects

Reach for a machine when named stages and legal transitions are the thing you are modelling — not when the thing is a value or a network cache.