Status: active Priority: P0 Created: 2026-02-20 Owner: AI Depends-on: session-id-convention, form-statechart, report-statechart, container-statechart, routing-conversion
The defsc-form, defsc-report, and defsc-container macros are the primary interface for downstream RAD users. They generate Fulcro component definitions with query, ident, initial-state, and dynamic routing hooks. These macros must be updated to:
::uism/asm-id in queries, UISM-based lifecycle hooks)sfro/statechart, sfro/busy?, sfro/initialize)will-enter, will-leave, allow-route-change?) since statecharts routing uses rstate/istate instead:route-segment -- route segments live only on istate in the routing chartCurrent macro implementations:
defsc-form* at form.cljc:587 with convert-options at form.cljc:528defsc-report at report.cljc:596defsc-container at container.cljc:136Routing options from statecharts library:
routing_options.cljc defines: sfro/initialize, sfro/initial-props, sfro/busy?, sfro/statechart, sfro/statechart-id, sfro/actorsistate in a statecharts routing chart[::uism/asm-id '_]sfro/statechart (or sfro/statechart-id) as a component optionfo/statechart / ro/statechart (renamed from fo/statechart / ro/statechart):will-enter, :will-leave, :allow-route-change? must not be generated (statecharts routing does not use them)defsc-form Macro RewriteThe current defsc-form via convert-options (line 528) generates:
;; Query (from form-options->form-query, line 389):
[id-key
:ui/confirmation-message
:ui/route-denied?
::form/errors
[::picker-options/options-cache '_]
[:com.fulcrologic.fulcro.application/active-remotes '_]
[::uism/asm-id '_] ;; <-- REMOVE
[fs/form-config-join] ;; keep (dirty tracking)
...attribute-keys...]
;; Ident:
(fn [_ props] [id-key (get props id-key)])
;; Initial State: (generated by pre-merge + form defaults)
;; Route lifecycle (when route-prefix is set):
:route-segment [route-prefix :action :id]
:allow-route-change? form-allow-route-change ;; <-- REMOVE
:will-leave (fn [this props] (form-will-leave this)) ;; <-- REMOVE
:will-enter (fn [app route-params] (form-will-enter app route-params form-class)) ;; <-- REMOVE
;; Route denied handler:
:route-denied (fn [this relative-root proposed-route timeouts-and-params]
(uism/trigger!! ...)) ;; <-- REMOVE (routing handles this)
;; Query -- remove ::uism/asm-id, keep everything else:
[id-key
:ui/confirmation-message
:ui/route-denied?
::form/errors
[::picker-options/options-cache '_]
[:com.fulcrologic.fulcro.application/active-remotes '_]
;; NO [::uism/asm-id '_]
[fs/form-config-join]
...attribute-keys...]
;; Ident -- UNCHANGED:
(fn [_ props] [id-key (get props id-key)])
;; Initial State -- UNCHANGED (pre-merge + form defaults still apply)
;; NO :route-segment (route segments live in the routing chart on istate)
;; NEW component options for statecharts routing:
sfro/statechart (or user-chart form/form-statechart)
sfro/busy? form-busy?
sfro/initialize :always
;; REMOVED:
;; :will-enter, :will-leave, :allow-route-change?, :route-denied
[::uism/asm-id '_]In form-options->form-query (line 389), remove the UISM asm-id join from query-with-scalars:
;; BEFORE:
(into [id-key :ui/confirmation-message :ui/route-denied? ::errors
[::picker-options/options-cache '_]
[:com.fulcrologic.fulcro.application/active-remotes '_]
[::uism/asm-id '_] ;; DELETE THIS LINE
fs/form-config-join]
...)
;; AFTER:
(into [id-key :ui/confirmation-message :ui/route-denied? ::errors
[::picker-options/options-cache '_]
[:com.fulcrologic.fulcro.application/active-remotes '_]
fs/form-config-join]
...)
sfro/statechartIn convert-options (line 528), add statechart routing options to the generated component:
;; The chart to use: user-specified via fo/statechart, or the default form-statechart
(let [chart (or (::statechart options) form-statechart)]
(assoc base-options
sfro/statechart chart
sfro/busy? form-busy?
sfro/initialize :always))
Where form-busy? is:
(defn form-busy?
"Returns true if the form has unsaved changes. Used by the routing system
to guard against navigating away from dirty forms."
[env data & _]
(let [{:actor/keys [form]} (scf/resolve-actors env :actor/form)]
(boolean (and form (fs/dirty? form)))))
The sfro/initialize :always setting means that every time the form is routed to, its state is re-initialized. This matches the current behavior where form-will-enter always starts a fresh UISM.
In convert-options, when route-prefix is set, the current code merges:
;; CURRENT (lines 567-572):
(merge {:route-segment [route-prefix :action :id]
:allow-route-change? form-allow-route-change
:will-leave (fn [this props] (form-will-leave this))
:will-enter (or will-enter
(fn [app route-params]
(form-will-enter app route-params (get-class))))})
Remove entirely. No :route-segment, no :will-enter, no :will-leave, no :allow-route-change?:
;; NEW:
;; No route lifecycle options generated at all.
;; Routing lifecycle is handled by istate + sfro/busy? + sfro/initialize.
;; Route segments are defined in the routing chart on istate, not on the component.
:route-denied DefaultThe current default route-denied handler (line 545) triggers the UISM directly. Remove it entirely. In the statecharts routing system, route denial is handled by the sfro/busy? function returning true, which causes the routing chart to set :route/denied? data that the form's statechart can react to.
fo/statechart to fo/statechartThe option is renamed from fo/statechart to fo/statechart to reflect the new semantics. It provides a statechart definition (the output of the statechart function):
;; BEFORE (UISM):
(defsc-form MyForm [this props]
{fo/statechart my-custom-uism-definition ;; a UISM map
...})
;; AFTER (Statechart):
(defsc-form MyForm [this props]
{fo/statechart my-custom-form-chart ;; a statechart definition
...})
The macro reads fo/statechart and places it as sfro/statechart:
sfro/statechart (or (::statechart options) form-statechart)
If the user specifies a keyword for fo/statechart (a pre-registered chart ID), it is placed as sfro/statechart-id instead:
(if (keyword? user-statechart)
(assoc base-options sfro/statechart-id user-statechart)
(assoc base-options sfro/statechart (or user-statechart form-statechart)))
defsc-report Macro RewriteFrom defsc-report (line 596):
;; Query (line 631):
[::report/id
:ui/parameters
:ui/cache
:ui/busy?
:ui/page-count
:ui/current-page
[::uism/asm-id [::report/id fqkw]] ;; <-- REMOVE
[::picker-options/options-cache '_]
{:ui/controls (comp/get-query Control)}
{:ui/current-rows subquery}
[df/marker-table '(quote _)]
...query-inclusions...]
;; Initial State (line 650):
{:ui/parameters {}
:ui/cache {}
:ui/controls (mapv ...)
:ui/busy? false
:ui/current-page 1
:ui/page-count 1
:ui/current-rows []}
;; Ident:
(fn [] [::report/id (or (::report/id props) fqkw)])
;; Route lifecycle (when route is set):
:will-enter (fn [app route-params] (report-will-enter app route-params report-class))
:route-segment (if (vector? route) route [route])
;; Query -- remove ::uism/asm-id:
[::report/id
:ui/parameters
:ui/cache
:ui/busy?
:ui/page-count
:ui/current-page
;; NO [::uism/asm-id ...]
[::picker-options/options-cache '_]
{:ui/controls (comp/get-query Control)}
{:ui/current-rows subquery}
[df/marker-table '(quote _)]
...query-inclusions...]
;; Initial State -- UNCHANGED
;; Ident -- UNCHANGED
;; NO :route-segment (route segments live in the routing chart on istate)
;; NEW component options:
sfro/statechart (or user-chart report/report-statechart)
sfro/initialize :once
;; REMOVED:
;; :will-enter (routing handled by istate)
[::uism/asm-id [::report/id fqkw]]Line 637 currently includes [::uism/asm-id [::id fqkw]]. Remove it.
sfro/statechart(let [chart (or (::statechart options) report-statechart)]
(assoc options
sfro/statechart chart
sfro/initialize :once))
Reports use :once initialization because they persist their state across route visits (the data stays loaded). Re-entering a report route should not restart the statechart from scratch.
:will-enterThe current default will-enter (line 645) calls report-will-enter which uses dr/route-deferred. Remove this entirely. The routing chart's istate handles initialization.
ro/statechart to ro/statechartSame pattern as forms. ro/statechart now provides a statechart definition or a pre-registered chart ID keyword:
(if (keyword? user-statechart)
(assoc options sfro/statechart-id user-statechart)
(assoc options sfro/statechart (or user-statechart report-statechart)))
defsc-container Macro RewriteFrom defsc-container (line 136):
;; Query (line 155):
[:ui/parameters
{:ui/controls (comp/get-query Control)}
[df/marker-table '(quote _)]
{child-id-1 (comp/get-query ChildSym1)}
{child-id-2 (comp/get-query ChildSym2)}
...]
;; Initial State (line 164):
{:ui/parameters {}
:ui/controls (mapv ...)
child-id-1 (comp/get-initial-state ChildSym1 {::report/id child-id-1})
child-id-2 (comp/get-initial-state ChildSym2 {::report/id child-id-2})}
;; Ident:
(fn [] [::container/id fqkw])
;; Route lifecycle (when route is set, line 169):
:route-segment [route]
:will-enter (fn [app route-params] (container-will-enter app route-params container-class))
;; Query -- UNCHANGED (containers have no ::uism/asm-id in their query)
;; Initial State -- UNCHANGED
;; Ident -- UNCHANGED
;; NO :route-segment (route segments live in the routing chart on istate)
;; NEW component options:
sfro/statechart (or user-chart container/container-statechart)
sfro/initialize :once
;; REMOVED:
;; :will-enter (routing handled by istate)
The container query does not include ::uism/asm-id. No modification required.
sfro/statechart(let [chart (or (comp/component-options container-class ::statechart) container-statechart)]
(assoc options
sfro/statechart chart
sfro/initialize :once))
Containers use :once initialization. Like reports, they persist across route visits.
:will-enterRemove container-will-enter usage. The routing chart's istate handles this.
The istate element in the routing chart handles chart registration automatically. From routing.cljc:446:
:srcexpr (fn [{:fulcro/keys [app]} data & _]
(let [Target (rc/registry-key->class target-key)
id (rc/component-options Target sfro/statechart-id)
chart (rc/component-options Target sfro/statechart)]
(cond
id id ;; pre-registered, use the ID
chart (do
(scf/register-statechart! app target-key chart)
target-key) ;; auto-register, use component key
:else (log/error "no statechart"))))
This means:
sfro/statechart (a chart definition), istate registers it at route time using the component's registry key as the chart keysfro/statechart-id (a keyword), istate assumes the chart is already registered under that keyfo/statechart (or ro/statechart)(def my-custom-form-chart
(statechart {:initial :state/loading}
...))
(defsc-form MyForm [this props]
{fo/id account/id
fo/attributes [...]
fo/statechart my-custom-form-chart})
;; Macro sets: sfro/statechart my-custom-form-chart
fo/statechart Keyword;; Somewhere at app init:
(scf/register-statechart! app ::my-special-chart my-chart-def)
(defsc-form MyForm [this props]
{fo/id account/id
fo/attributes [...]
fo/statechart ::my-special-chart})
;; Macro sets: sfro/statechart-id ::my-special-chart
fo/statechart)(defsc-form MyForm [this props]
{fo/id account/id
fo/attributes [...]})
;; Macro sets: sfro/statechart form/form-statechart (the library default)
Most users who use the default form/report machines need zero changes. The macros generate the correct component options automatically.
fo/statechart / ro/statechart OverridesThese users must rewrite their custom machines as statecharts. This is a breaking change, but it is expected and documented. The old UISM definition format is incompatible with the new statechart format.
:will-enter OverridesSome forms specify a custom :will-enter in the macro options. These overrides will be silently ignored since the macro no longer generates :will-enter. Users should be warned at macro expansion time:
(when (contains? options :will-enter)
(log/warn "defsc-form" sym ":will-enter is ignored. Routing lifecycle is managed by statecharts routing."))
::uism/asm-idAny code that reads ::uism/asm-id from component props will get nil. This affects:
These must be updated to use scf/current-configuration instead.
src/main/com/fulcrologic/rad/form.cljc - defsc-form*, convert-options, form-options->form-querysrc/main/com/fulcrologic/rad/report.cljc - defsc-report macrosrc/main/com/fulcrologic/rad/container.cljc - defsc-container macrosrc/main/com/fulcrologic/rad/form_options.cljc - Rename fo/machine to fo/statechart, update docstringsrc/main/com/fulcrologic/rad/report_options.cljc - Rename ro/machine to ro/statechart, update docstringfo/statechart renamed to fo/statechart (and ro/statechart to ro/statechart). The rename makes the intent clear. All references to fo/statechart / ro/statechart in the macros and option namespaces become fo/statechart / ro/statechart. This is a breaking change for users who specified custom machines, but they must rewrite their custom machines as statecharts anyway.:route-segment in macros. Route segments live only on istate in the routing chart. The macros no longer generate :route-segment. Users define route segments in their routing chart definition.:will-enter overrides? Yes, at macro expansion time.defsc-form generates query without ::uism/asm-iddefsc-form generates sfro/statechart component option pointing to form-statechartdefsc-form generates sfro/busy? component option pointing to form-busy?defsc-form generates sfro/initialize :alwaysdefsc-form does not generate :will-enter, :will-leave, :allow-route-change?defsc-form with fo/statechart custom chart sets sfro/statechart to user chartdefsc-form with fo/statechart keyword sets sfro/statechart-id to that keyworddefsc-report generates query without ::uism/asm-iddefsc-report generates sfro/statechart pointing to report-statechartdefsc-report generates sfro/initialize :oncedefsc-report does not generate :will-enterdefsc-container generates sfro/statechart pointing to container-statechartdefsc-container generates sfro/initialize :oncedefsc-container does not generate :will-entersfro/busy? correctly prevent route changes when dirtyistate auto-registers charts from sfro/statechart component option:will-enter override detectedCan you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |