Status: backlog Priority: P1 Created: 2026-02-20 Owner: conductor Depends-on: project-setup, form-statechart, report-statechart
com.fulcrologic.rad.rad-hooks provides use-form and use-report -- React hooks that allow embedding RAD forms and reports into arbitrary React components without routing. They currently depend on Fulcro UI State Machines (UISMs) for lifecycle management. These must be converted to use statecharts.
The statecharts library already provides com.fulcrologic.statecharts.integration.fulcro.hooks/use-statechart, a React hook that manages a statechart session tied to a component's lifecycle. The RAD hooks should become thin wrappers around use-statechart, translating RAD-specific options into statechart session configuration.
use-form (rad_hooks.cljc:16-72)Current behavior:
hooks/use-generated-idrc/nc) that joins the form datahooks/use-component to subscribe to form propsuism/begin! with :embedded? true, passing save/cancel mutation callbacksuism/remove-uism!{:form-factory, :form-props, :form-state}Key UISM integration points:
fo/statechart -- optional custom form machine overrideform/form-machine -- default UISM machineuism/begin! with actor :actor/form:on-saved, :on-cancel, ::form/create?::uism/active-state in container propsuse-report (rad_hooks.cljc:74-90)Current behavior:
comp/get-identhooks/use-component to subscribe to report propsreport/start-report! with :embedded? true:keep-existing?){:report-factory, :report-props, :report-state}use-statechart (statecharts hooks.cljc:10-62)The statecharts use-statechart hook:
this (component instance) and start args including optional session-id:actor/component:statechart optionscf/start! if not already running:event/unmount to the chart{:send!, :config, :local-data, :aliases}use-form must start a form statechart session (not a UISM) on mount and clean it up on unmountuse-report must start a report statechart session (not a UISM) on mount and clean it up on unmount{:form-factory, :form-props, :form-state} and {:report-factory, :report-props, :report-state}.cljc -- hooks are React-only at runtime but the namespace must compile on CLJ for headless testing stubssrc/main/com/fulcrologic/rad/rad_hooks.cljc - Primary conversion targetsrc/main/com/fulcrologic/rad/form_statechart.cljc - Form statechart (must support embedded/hook mode)src/main/com/fulcrologic/rad/report_statechart.cljc - Report statechart (must support embedded/hook mode)src/test/com/fulcrologic/rad/rad_hooks_test.cljc - Tests for hook behavioruse-statechart (Recommended)Make use-form and use-report delegate to use-statechart from the statecharts library. The RAD hooks add RAD-specific setup:
:actor/form or :actor/report){:send!, :config, :local-data, :aliases} into the RAD-specific return shape(defn use-form
[app-ish Form id save-complete-mutation cancel-mutation
{:keys [save-mutation-params cancel-mutation-params]}]
(let [app (rc/any->app app-ish)
id-key (-> Form rc/component-options fo/id ao/qualified-key)
form-ident [id-key id]
session-id (hooks/use-generated-id)
;; Start the form statechart with proper actors
{:keys [send! config local-data aliases]}
(use-statechart-for-rad app
{:statechart (or (rc/component-options Form :statechart)
form/default-form-chart)
:session-id session-id
:data {:fulcro/actors {:actor/form (scf/actor Form form-ident)}
:on-saved save-complete-mutation
:on-cancel cancel-mutation
:save-params save-mutation-params
:cancel-params cancel-mutation-params
:create? (tempid/tempid? id)}})]
{:form-factory (comp/computed-factory Form {:keyfn id-key})
:form-props (get-in (app/current-state app) form-ident)
:form-state (statechart-state->form-state config)
:send! send!}))
Manage scf/start!, scf/send!, and cleanup directly in the hooks, similar to how the current code manages UISM lifecycle. This avoids depending on use-statechart (which assumes a co-located :statechart key on the component) but duplicates lifecycle management.
sfro/statechart on the Component (Simplified)Since defsc-form and defsc-report macros now set sfro/statechart as a component option (see macro-rewrites.md), the use-statechart hook from the statecharts library works directly -- it reads the chart from the component's sfro/statechart option. No adapter or extraction of lower-level hooks is needed.
The RAD hooks become thin wrappers:
use-statechart with the Form/Report instance{:form-factory, :form-props, :form-state} shape:embedded? true, save/cancel callbacks) via the start dataWhen started via hooks (not routing), the form/report statechart needs to know it's embedded. The :embedded? true flag is passed in session data:
;; In use-form start data:
{:embedded? true
:on-saved save-complete-mutation
:on-cancel cancel-mutation
...}
The form statechart checks (:embedded? data) in expressions to:
sfro/busy? is irrelevant):on-saved / :on-cancel mutations from session dataOn unmount, use-statechart sends :event/unmount to the chart. The form/report statechart must handle this event by transitioning to a final state, which triggers automatic GC of the session from Fulcro state. This replaces the current uism/remove-uism! call.
;; In the form/report statechart:
(on :event/unmount :state/done)
(final {:id :state/done})
The final state causes the statecharts library to automatically clean up the session data from Fulcro state, preventing memory leaks.
Each hook invocation needs a unique session ID:
hooks/use-generated-id to create a stable session ID per component instance. This prevents collisions when multiple forms are mounted simultaneously.[::ReportName :singleton]) can use a deterministic session ID derived from the ident. For multiple instances, use hooks/use-generated-id.session-id is explicitly provided, the hook should reconnect to an existing session (send no start event if already running). This matches use-statechart's current behavior.The hooks file must compile on CLJ for the following reasons:
Strategy:
.cljc#?(:cljs ...) reader conditionals(defn use-form
"React hook. Use a RAD form backed by a statechart session.
CLJ: Not callable -- throws. Use statechart testing utilities directly."
[app-ish Form id save-complete-mutation cancel-mutation & [options]]
#?(:cljs
(let [...] ;; actual hook implementation
{:form-factory ...
:form-props ...
:form-state ...})
:clj
(throw (ex-info "use-form is a React hook and cannot be called on the JVM. Use statechart testing utilities instead." {}))))
The hooks must map statechart configuration to the legacy return shape:
| Legacy key | Source |
|---|---|
:form-factory | (comp/computed-factory Form {:keyfn id-key}) |
:form-props | Resolved actor data from Fulcro state |
:form-state | Mapped from active statechart configuration |
:report-factory | (comp/computed-factory Report {:keyfn pk}) |
:report-props | hooks/use-component for report subscription |
:report-state | Mapped from active statechart configuration |
The :form-state / :report-state values were previously UISM active-state keywords. We need a mapping from statechart states to equivalent keywords so downstream code that switches on form/report state continues to work. For example:
(defn statechart-state->form-state
"Maps statechart configuration to legacy form state keywords for backward compatibility."
[config]
(cond
(contains? config :state/editing) :state/editing
(contains? config :state/saving) :state/saving
(contains? config :state/loading) :state/loading
:else :state/initial))
Currently, save/cancel invoke Fulcro mutations directly. In the statechart model:
:event/saved or :event/cancelled when done:data map passed to scf/start!The statechart's on-entry for the "saved" state would look at session data for the callback mutation and invoke it:
(on-entry {}
(script {:expr (fn [env data]
(when-let [on-saved (:on-saved data)]
[(fops/invoke-remote on-saved
{:params (merge (:save-params data)
{:ident (:form-ident data)})})]))}))
use-statechart in the statecharts library be refactored to expose a lower-level use-statechart-session hook, or should the RAD hooks manage the lifecycle independently?use-form supports a custom :fo/statechart override on the Form component. Should this be preserved as a :statechart override, or should all forms use a single canonical form statechart?:form-state / :report-state return values be the raw statechart configuration set, or the legacy keyword mapping? Raw is more powerful but breaks backward compatibility.use-form's :embedded? true flag translate to the statechart model? The form statechart likely needs to know it was started by a hook (not routing) to skip route-exit behavior.use-report calls report/start-report! which does UISM + data loading. In the statechart model, should data loading be triggered by the report statechart's initial state entry, or should the hook explicitly load?send! function to the return map so callers can send arbitrary events to the form/report statechart? This would be a new capability not available in the UISM model.use-form starts a statechart session (not a UISM) on mountuse-form cleans up the session on unmountuse-form returns {:form-factory, :form-props, :form-state} with correct valuesuse-form correctly handles create (tempid) vs edit (real id)use-form save/cancel callbacks are invoked via statechart completion statesuse-report starts a statechart session on mountuse-report returns {:report-factory, :report-props, :report-state}use-report respects :keep-existing? on unmountsfro/statechart on component (no need to extract use-statechart-session hook):embedded? true in session data, skip route-exit, use callback mutations:event/unmount -> chart reaches final state -> automatic GCCan 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 |