Status: backlog Priority: P0 Created: 2026-02-20 Owner: conductor Depends-on: project-setup, routing-conversion, form-statechart, report-statechart, container-statechart
Applications built on fulcro-rad currently initialize routing and UI machinery through a combination of install-routing!, install-route-history!, and rad/install!. The statecharts-based system replaces all of these with the statecharts routing library: scf/install-fulcro-statecharts!, scr/start!, and scr/install-url-sync!.
This spec defines the complete bootstrap sequence for a fulcro-rad-statecharts application, covering both browser and headless (testing) modes.
(require
'[com.fulcrologic.fulcro.application :as app]
'[com.fulcrologic.statecharts.integration.fulcro :as scf]
'[com.fulcrologic.statecharts.integration.fulcro.routing :as scr]
'[com.fulcrologic.statecharts.integration.fulcro.routing-options :as sfro]
'[com.fulcrologic.statecharts.integration.fulcro.routing.url-codec-transit :as ruct]
'[com.fulcrologic.statecharts.chart :refer [statechart]]
'[com.fulcrologic.statecharts.elements :refer [state final parallel]]
'[com.fulcrologic.rad.application :as rad-app])
install-routing! and install-route-history!rad/install! (the RAD application setup function)| Current RAD Function | Replacement | Notes |
|---|---|---|
install-routing! | scr/start! | Registers and starts the routing statechart |
install-route-history! | scr/install-url-sync! | Bidirectional URL synchronization |
DR will-enter / will-leave | rstate / istate on-entry/on-exit | Route lifecycle managed by chart elements |
route-to! (DR-based) | scr/route-to! | Sends route-to event to routing statechart |
uism/begin! in route entry | istate invoke | Statechart auto-invoked on route entry |
rad-app/fulcro-rad-app -- creates the Fulcro application (unchanged)rad-app/install-ui-controls! -- installs rendering control plugins (unchanged)app/mount! -- mounts the Fulcro app to the DOM (unchanged)No changes here. The app is still created with rad-app/fulcro-rad-app:
(defonce app (rad-app/fulcro-rad-app {}))
No changes. Controls are installed exactly as before:
(rad-app/install-ui-controls! app all-controls)
This stores the control map in the Fulcro runtime atom under :com.fulcrologic.rad/controls. The statecharts system does not interact with controls directly -- they are consumed by form/report renderers at render time.
This replaces all UISM infrastructure. Call scf/install-fulcro-statecharts! with the app and options:
(scf/install-fulcro-statecharts! app
{:on-save (fn [session-id wmem]
(scr/url-sync-on-save session-id wmem app))})
Key options:
:on-save -- Required for URL sync. Called every time a statechart reaches a stable state and its working memory is saved. scr/url-sync-on-save must be called here, passing app as the third argument to enable child chart URL tracking. You may compose additional on-save logic (e.g., session durability):
{:on-save (fn [session-id wmem]
(scr/url-sync-on-save session-id wmem app)
(my-persistence/save! session-id wmem))}
:on-delete -- Optional (fn [session-id]) called when a session reaches a final state and is GC'd.
:event-loop? -- Controls event processing mode:
true (default) -- Starts a core.async event loop. Use in browser.false -- Manual processing via scf/process-events!. Use in headless tests.:immediate -- Synchronous processing during send!. Use in CLJ tests that need immediate state transitions without manual polling.:extra-env -- A map merged into every expression's env argument. Use for injecting services:
{:extra-env {:my-service (create-service)}}
:async? -- If true, uses promesa-based async processor. Enables afop/await-load and afop/await-mutation. Defaults to false.
What this does internally:
::sc/env(fn [env data event-name event-data])Individual form and report statecharts are typically co-located on their component classes via the sfro/statechart component option. They are auto-registered during routing when istate invokes them (the srcexpr in istate calls scf/register-statechart! lazily).
For statecharts that need explicit registration (e.g., a custom auth chart):
(scf/register-statechart! app ::auth-chart auth-statechart)
The routing chart is the central piece that replaces DR's routing tree. It defines the application's route structure using scr/routes, scr/rstate, and scr/istate.
(def routing-chart
(statechart {:initial :state/route-root}
(scr/routing-regions
(scr/routes {:id :region/main :routing/root :my.app/Root}
;; Simple route (no co-located statechart)
(scr/rstate {:route/target :my.app/Dashboard})
;; Route with co-located statechart (form/report)
(scr/istate {:route/target :my.app/AccountForm
:route/segment "account"
:route/params #{:id}})
;; Route with nested routes
(scr/rstate {:route/target :my.app/AdminPanel :parallel? true}
(scr/routes {:id :region/admin :routing/root :my.app/AdminPanel}
(scr/istate {:route/target :my.app/UserList})
(scr/istate {:route/target :my.app/AuditReport})))))))
Key routing elements:
scr/routing-regions -- Wraps routes in a parallel state with routing info management (route-denied modal support). Returns a :state/route-root containing :state/top-parallel.
scr/routes -- A state representing a routing region. Options:
:id -- State ID (e.g., :region/main):routing/root -- The component that serves as the root of this routing region. Must have a constant ident (or be the app root).scr/rstate -- A simple routing state. The :route/target is a component registry key. The state ID is derived from the target (do NOT pass :id). Handles component initialization and parent query updates on entry.
scr/istate -- A routing state that invokes a co-located statechart on the target component. The component must have sfro/statechart or sfro/statechart-id set. Handles everything rstate does, plus starts the child statechart session.
How fo/route-prefix maps to the routing chart:
The RAD form option fo/route-prefix (e.g., "account") maps to :route/segment on rstate/istate. If not specified, defaults to the simple name of the target component's registry key.
How route parameters work:
Route parameters are declared via :route/params (a set of keywords). When a route-to event carries data matching those keywords, the values are stored in the data model at [:routing/parameters <state-id>]. The URL codec encodes these parameters into the URL.
(scr/start! app routing-chart)
This:
scr/session-id (which is ::scr/session)Optional second argument for validation mode:
(scr/start! app routing-chart {:routing/checks :strict})
:warn (default) -- Logs warnings for configuration issues:strict -- Throws on configuration issues(scr/install-url-sync! app)
Call AFTER scr/start! completes. This sets up bidirectional synchronization between the routing statechart state and the browser URL.
Options:
(scr/install-url-sync! app
{:prefix "/"
:url-codec (ruct/transit-base64-codec)
:on-route-denied (fn [url] (log/warn "Route denied:" url))})
:provider -- A URLHistoryProvider. Defaults to (bh/browser-url-history) on CLJS. Required on CLJ (must provide a simulated history).:url-codec -- A URLCodec for encoding/decoding URLs. Defaults to (ruct/transit-base64-codec).:prefix -- URL path prefix (default "/").:on-route-denied -- (fn [url]) called when back/forward navigation is denied by the busy guard.URL sync mechanics:
url-sync-on-save handler (installed in step 3) computes the URL from the current configuration and pushes/replaces it in the browser history.route-to event to the routing statechart.Returns a cleanup function that removes all listeners.
No changes:
(app/mount! app Root "app")
(ns my.app.client
(:require
[com.fulcrologic.fulcro.application :as app]
[com.fulcrologic.statecharts.integration.fulcro :as scf]
[com.fulcrologic.statecharts.integration.fulcro.routing :as scr]
[com.fulcrologic.statecharts.chart :refer [statechart]]
[com.fulcrologic.rad.application :as rad-app]
[my.app.ui.root :refer [Root]]
[my.app.ui.controls :refer [all-controls]]
;; Require component namespaces so they register
[my.app.ui.account-form]
[my.app.ui.dashboard]
[my.app.ui.user-list]))
(defonce app (rad-app/fulcro-rad-app {}))
(def routing-chart
(statechart {:initial :state/route-root}
(scr/routing-regions
(scr/routes {:id :region/main :routing/root :my.app.ui.root/Root}
(scr/rstate {:route/target :my.app.ui.dashboard/Dashboard})
(scr/istate {:route/target :my.app.ui.account-form/AccountForm
:route/segment "account"
:route/params #{:id}})
(scr/istate {:route/target :my.app.ui.user-list/UserList
:route/segment "users"})))))
(defn ^:export init []
;; 1. Install rendering controls
(rad-app/install-ui-controls! app all-controls)
;; 2. Install statecharts infrastructure
(scf/install-fulcro-statecharts! app
{:on-save (fn [session-id wmem]
(scr/url-sync-on-save session-id wmem app))})
;; 3. Start the routing chart
(scr/start! app routing-chart)
;; 4. Install URL synchronization
(scr/install-url-sync! app)
;; 5. Mount the app
(app/mount! app Root "app"))
(ns my.app.test-helpers
(:require
[com.fulcrologic.fulcro.application :as app]
[com.fulcrologic.statecharts.integration.fulcro :as scf]
[com.fulcrologic.statecharts.integration.fulcro.routing :as scr]
[com.fulcrologic.statecharts.integration.fulcro.routing.simulated-history :as sim]
[com.fulcrologic.statecharts.chart :refer [statechart]]
[com.fulcrologic.rad.application :as rad-app]))
(defn create-test-app
"Creates a fully initialized test app with routing. Returns the app.
The app uses :immediate event processing so all transitions are synchronous."
[routing-chart & [{:keys [controls]}]]
(let [test-app (rad-app/fulcro-rad-app {})]
;; Set the root so Fulcro state is initialized
(app/set-root! test-app Root {:initialize-state? true})
;; Install controls if provided
(when controls
(rad-app/install-ui-controls! test-app controls))
;; Install statecharts with :immediate event processing (no async, no polling)
(scf/install-fulcro-statecharts! test-app
{:event-loop? :immediate
:on-save (fn [session-id wmem]
(scr/url-sync-on-save session-id wmem test-app))})
;; Start routing
(scr/start! test-app routing-chart)
test-app))
(defn create-test-app-with-url-sync
"Like create-test-app but also installs URL sync with a simulated history provider.
Returns {:keys [app provider]} so tests can inspect URL state."
[routing-chart & [opts]]
(let [test-app (create-test-app routing-chart opts)
provider (sim/simulated-url-history)]
(scr/install-url-sync! test-app {:provider provider})
{:app test-app :provider provider}))
Key differences from browser mode:
| Aspect | Browser | Headless |
|---|---|---|
:event-loop? | true (core.async loop) | :immediate (synchronous) |
| URL sync provider | Browser history (auto) | Simulated history (manual) |
app/mount! | Called with DOM element | Not called; use app/set-root! |
| Component registration | Requires from ns | Same -- must require component ns |
rad/install!The current RAD application module does NOT have a single install! function. The setup is composed from individual calls:
rad-app/fulcro-rad-app -- Creates the app (unchanged)rad-app/install-ui-controls! -- Installs controls (unchanged)The default-network-blacklist in rad-app currently includes ::uism/asm-id. In the statecharts system, this entry becomes irrelevant (no asm-id in queries), but it is harmless to leave in place. The blacklist may need ::sc/session-id added if statechart session data appears in queries, but since statechart working memory is stored at [::sc/session-id session-id] (a table, not a component prop), it will not appear in normal component queries.
Rendering controls are initialized identically to current RAD. The install-ui-controls! function stores the control map in the app runtime atom. Form and report renderers access controls at render time via (-> app ::app/runtime-atom deref :com.fulcrologic.rad/controls).
The statecharts system does not change how controls are stored, looked up, or used. Controls are a rendering concern orthogonal to state management.
The statecharts routing system has built-in support for route denial when forms are dirty:
sfro/busy? as a component option, or the system auto-detects dirty forms via fs/dirty?busy? returns true, the routing chart stores the failed route event and raises :event.routing-info/show(scr/route-denied? app) returns true(scr/force-continue-routing! app) to force the route change(scr/abandon-route-change! app) to cancel and stay on the current routeThis replaces the UISM-based route-denied?, continue-abandoned-route!, and clear-route-denied! functions.
fulcro-rad-app be updated to optionally call install-fulcro-statecharts! automatically, or should it remain a separate step? Recommendation: keep separate for clarity and testability.rstate/istate. RAD does NOT auto-generate the routing chart from form/report definitions. Auto-generation may be considered for a future version, but for v1 the chart is explicitly authored by the application developer.install-ui-controls! works unchangedfulcro-rad-app works unchanged[:routing/parameters <state-id>]Can 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 |