Skip to content

Tags

A view often does not care which exact state a machine is in. It cares about a semantic question: is this flow busy? read-only? terminal?

A state tag is a label on a state that answers those questions without the view enumerating state names. The first machine already tags :submitting with :auth/busy.

Declare tags on states

Start from the login table. Tag the states the view actually asks about:

:submitting  {:tags #{:auth/busy}   …}
:authed      {:tags #{:auth/authed}
              :meta {:terminal? true}}
:locked-out  {:tags #{:auth/locked :auth/terminal}
              :meta {:terminal? true}}

If login later grows a second in-flight state (refreshing a token, restoring a session), put #{:auth/busy} on that state too. The view that asks for the tag does not change.

:tags is a set of keywords on a state node. A vector or a lone keyword is rejected at registration with :rf.error/machine-bad-tags.

Tags label intent, not identity. Good tags name what the state means to the rest of the program:

:tags #{:auth/busy}
:tags #{:mode/read-only}
:tags #{:ws/connected}

Weak tags repeat the state's identity:

:tags #{:auth-login/submitting-state}

Use a per-axis namespace (:data/…, :form/…, :mode/…) so one question can span several states. The :rf/* and :rf.*/* namespaces are reserved, and a tag in them is :rf.error/machine-bad-tags too; tag with your own feature prefix. Dotted forms such as :ui.state/loading are fine.

Read :state directly when the caller needs that exact state, rather than a label that may apply to more states as the flow grows.

Query one tag

The query is this subscription:

@(rf/subscribe [:rf.machine/has-tag? :auth.login/flow :auth/busy])
;; => true or false
(rf/reg-view sign-in-button []
  (let [busy? @(subscribe [:rf.machine/has-tag? :auth.login/flow :auth/busy])]
    [:button {:disabled busy?} "Sign in"]))

It returns false for an unknown or not-yet-initialised machine. The sub is derived: a view that asks one tag re-renders when that membership flips, not on every :data write.

Need the whole set? Read the snapshot:

(:tags @(rf/subscribe [:rf/machine :auth.login/flow]))
;; => #{:auth/busy}

Use that form for selectors and render-priority tables.

The snapshot's :tags

The runtime unions the tags on every active state and writes the result onto the snapshot:

@(rf/subscribe [:rf/machine :auth.login/flow])
;; => {:state :submitting
;;     :data  {...}
;;     :tags  #{:auth/busy}}

How the union is computed depends on the machine's shape:

Machine shape Tag projection
Flat tags on the active state
Hierarchical union along the active path, root to leaf
Parallel union of every active state in every region

Tags declared on the machine root join the union in every shape.

The runtime owns :tags. An action cannot return {:tags …} — the slot is a projection of :state. When no active state declares tags, the runtime elides the key. Do not declare :tags #{} to force the slot; omit it.

Troubleshooting

Symptom Cause Fix
Registration throws :rf.error/machine-bad-tags :tags is a vector or a lone keyword, or a tag is in :rf/* / :rf.*/* Write a set of your own tags: #{:data/in-flight}
Snapshot has no :tags key No active state declares tags; the empty union is elided Omit the key; (contains? (:tags snap) x) is still false
Guard ctx has no :tags / :all-state The machine is flat or compound Those keys exist only inside a parallel region

Advanced

Tags as a cross-region signal

In a parallel machine a region's guards and actions receive the machine-wide tag union as :tags, so one region can read another's state by tag without knowing its state names. A tag appearing fires nothing; a guard reads it when it runs. Coordinating regions has the example.

What tags are not

  • Not transition labels. :tags is a state-node slot. Transitions carry none.
  • Not :meta. A state's :meta (for example {:terminal? true}) is static, tooling-visible metadata. :tags is the live projection of the active configuration. Both can sit on the same state.

Render priority

Several tags can be live at once in a parallel machine, but a page can render only one main view.

The Nine States example is one :type :parallel machine with three regions — :data, :form, :mode. Each state advertises a per-axis tag:

;; cf. examples/patterns/nine_states
:loading   {:tags #{:data/loading :data/transient} :on {...}}
:empty     {:tags #{:data/empty}                   :on {...}}
:incorrect {:tags #{:form/invalid}                 :on {...}}
:correct   {:tags #{:form/success :form/transient} :on {...}}
:done      {:tags #{:mode/done :mode/read-only :mode/terminal}}

Make the tie-breaker plain data: a table read top to bottom, plus a selector sub that returns the first matching tag's render keyword.

;; cf. examples/patterns/nine_states — the example carries all ten rows
(def render-priority
  [{:tag :mode/done    :render :done}
   {:tag :form/success :render :correct}
   {:tag :form/invalid :render :incorrect}
   {:tag :data/loading :render :loading}
   {:tag :data/error   :render :error}
   {:tag :data/empty   :render :empty}
   {:tag :data/some    :render :some}])

(rf/reg-sub :ui/render {:inputs [[:rf/machine :ui/nine-states]]}
  (fn [[snap] _]
    (let [tags (:tags snap)]
      (some (fn [{:keys [tag render]}]
              (when (contains? tags tag) render))
            render-priority))))

The root view branches once:

(rf/reg-view root-view []
  (case @(subscribe [:ui/render])
    :done      [view-done]
    :correct   [view-correct]
    :incorrect [view-incorrect]
    :loading   [view-loading]
    :error     [view-error]
    :empty     [view-empty]
    :some      [view-some]
    [:p "(unrecognised state)"]))

The order is a product decision — archived beats a form acknowledgement beats the data bucket — living in one table. Adding a render case is one row plus one case clause.

(rf/reg-view new-todo-form []
  (let [read-only? @(subscribe [:rf.machine/has-tag? :ui/nine-states :mode/read-only])]
    [:button {:disabled read-only?} "Add"]))

The form does not ask whether :mode is :done. It asks whether the screen is read-only.