7. History¶
The session machine forgets which signed-in screen
you were on. Logout lands on :unauthenticated. The next success always
opens :dashboard, even if you left from :settings.
A history state lets a compound remember where it was when you left it.
History is only for compound states. If the remembered thing is a flat value,
store it in :data.
A history state is a target, not a place you sit¶
A history state is a pseudo-state declared under a compound's :states.
The machine never occupies it. A transition targets it, and the runtime resolves
that target to a real child. The snapshot's :state
records the resolved leaf — never the pseudo-state.
Example: last signed-in screen¶
(rf/reg-machine :auth.login/flow
{:initial :unauthenticated
:states
{:unauthenticated
{:initial :idle
:states
{:idle
{:on {:auth.login/submit :submitting}}
:submitting
{:on {:auth.login/success [:authenticated :hist]
:auth.login/failure :error-shown}}
:error-shown
{:on {:auth.login/dismiss :idle}}}}
:authenticated
{:initial :dashboard
:on {:auth.logout [:unauthenticated]}
:states
{:hist
{:type :history
:deep? true
:default-target :dashboard}
:dashboard
{:on {:open-settings :settings}}
:settings
{:on {:close :dashboard}}}}}})
Drive it with dispatch-sync so each line has settled before the next:
(rf/dispatch-sync [:auth.login/flow [:auth.login/submit]])
(rf/dispatch-sync [:auth.login/flow [:auth.login/success]])
;; => [:authenticated :dashboard]
(rf/dispatch-sync [:auth.login/flow [:open-settings]])
;; => [:authenticated :settings]
(rf/dispatch-sync [:auth.login/flow [:auth.logout]])
;; => [:unauthenticated :idle], recording :authenticated
(rf/dispatch-sync [:auth.login/flow [:auth.login/submit]])
(rf/dispatch-sync [:auth.login/flow [:auth.login/success]])
;; => restores [:authenticated :settings]
@(rf/subscribe [:rf/machine :auth.login/flow])
;; => {:state [:authenticated :settings]
;; :data {}
;; :rf/history {[:authenticated] [:authenticated :settings]}}
The target [:authenticated :hist] means "enter :authenticated through
its history pseudo-state." First login has no recording, so
:default-target opens :dashboard.
The keys¶
| Key | Meaning |
|---|---|
:type :history |
Marks the node as a history pseudo-state. Required. |
:deep? true |
Restore the full nested path. Absent or false means shallow. |
:default-target |
Where to go before anything has been recorded. Absent ⇒ the owning compound's :initial. |
A history pseudo-state cannot declare :on, :entry, :exit, :always,
:after, :spawn, :spawn-all, :states, :initial, :tags, or :final?.
It is not a real state. Any extra key is :rf.error/machine-history-extra-keys
at reg-machine time.
A keyword :default-target names a direct child of the owning compound. Use a
vector for an absolute path.
Shallow vs deep¶
Suppose you leave :authenticated from :settings.
Deep history records the full leaf path:
Restoring returns to [:authenticated :settings].
Shallow history records only the direct child of the owning compound:
If :settings were itself a compound, restoring would enter :settings
and then follow its :initial. For a leaf, deep and shallow land in the
same place.
Use deep when the precise nested position matters. Use shallow when only the top-level branch matters.
Recording happens on exit¶
History records when the owning compound is actually exited.
In the example, :auth.logout leaves :authenticated for
:unauthenticated, so the runtime records :authenticated's last
configuration.
A transition between :dashboard and :settings does not record history,
because :authenticated was never exited. It remained the
least common ancestor.
If history is not sticking, check that the transition leaves the compound that owns the history node.
Restore order¶
When a transition targets a history pseudo-state, the runtime resolves it in this order:
- use a valid recording, if one exists
- otherwise use
:default-target, if present - otherwise use the owning compound's
:initial
If a hot reload removes the recorded target, the runtime discards the stale recording and falls back to the default. That is not an error.
Once resolved, the normal exit/entry cascade runs. History is target resolution, not a separate transition mechanism.
The :rf/history snapshot slot¶
History recordings live in a runtime-owned snapshot slot:
{:state [:authenticated :dashboard]
:data {...}
:rf/history {[:authenticated] [:authenticated :settings]}}
The key is the compound's declaration path. The value is either a full path for deep history or a direct child keyword for shallow history.
You do not write this slot. It is part of the snapshot, so it participates in undo, time-travel, persistence, and SSR hydration.
Parallel regions¶
Inside a parallel machine, history is scoped to the region
that owns it. The :rf/history key is region-qualified so recordings do not
collide.
The region name heads the key; the value is the within-region path. Restoring history in one region leaves the others alone.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
| History never restores; always hits default | Owning compound never left (sibling moves keep it as LCA) | Give the compound an off-state outside it (:unauthenticated next to :authenticated) |
View cases on :hist |
Pseudo-state is never in :state |
Transition to :hist resolves to a real leaf — case on that |
| Registration error at root / bare parallel | History needs an enclosing compound | Nest under a compound with :states + :initial. Error: :rf.error/machine-history-misplaced |
| Two history children under one compound | At most one history node per compound | Use one node; deep vs shallow is :deep?. Error: :rf.error/machine-history-duplicate |
Unresolvable :default-target |
Keyword form must name a direct child | Use a direct-child keyword, or a vector for an absolute path. Error: :rf.error/machine-history-bad-default-target |
| Extra keys on the history node | The pseudo-state is never occupied | Only :type, :deep?, :default-target. Error: :rf.error/machine-history-extra-keys |