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:
Weak tags repeat the state's identity:
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/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:
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.
:tagsis a state-node slot. Transitions carry none. - Not
:meta. A state's:meta(for example{:terminal? true}) is static, tooling-visible metadata.:tagsis 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.