Ephemeral state: where it belongs¶
A dropdown can be open, a field can hold a half-typed draft, and a drag can have an in-flight pointer position. Those facts do not all belong in the same place.
Hicasso has no component-local reactive cell. There is no Hicasso equivalent
of Reagent's r/atom, and useState does not belong in a defview body.
Application-visible facts live in app-db. High-rate widget mechanics stay
inside a native host. Browser-owned state stays in the browser.
That gives each fact one owner and keeps a second reactive store out of the application.
Why one owner matters¶
Three re-frame2 properties depend on it.
Tests stay data-driven. If “this dropdown is open” is stored at an app-db address, a headless test can seed that address directly. It does not need to mount a component, simulate a click, or wait for a timer (Testing).
Diagnostics keep a complete cause chain. Xray can connect an event to a state commit, subscription invalidation, view render, React commit, and paint. A private reactive store updates on a clock outside that chain (Diagnostics).
Frames remain isolated. App-db is per-frame. A module-level atom is shared by every frame that mounts the code, which defeats frame isolation.
A host may still keep React state, DOM state, canvas state, or an SDK handle. It must not keep an invisible duplicate of an application fact.
1. Application-visible state: app-db¶
Use an explicit app-db address for state that affects what the application can do or what another part of the application can observe: open, expanded, selected, or the active tab.
One parameterised event and subscription can serve every instance:
(ns app.panels
(:require [re-frame.core :as rf]
[re-frame.hicasso :as h]))
(rf/reg-sub :panel/expanded?
(fn [db [_ panel-id]]
(get-in db [:ui :panel/expanded panel-id] false)))
(rf/reg-event :panel/toggled
(fn [{:keys [db]} [_ panel-id]]
{:db (update-in db [:ui :panel/expanded panel-id] not)}))
(h/defview panel [{:keys [id title]}]
[:section
[:h3 {:on-click [:panel/toggled id]} title]
(when (h/sub [:panel/expanded? id])
[panel-body {:id id}])])
A hundred panels reuse the same event and subscription. The address gives you replay, frame isolation, Xray visibility, and direct test setup.
Prefer named events such as [:panel/toggled id] over a generic
[:ui/set path value]. The named event records what happened and leaves room
for effects or related state changes later.
2. Drafts and form state: the forms module¶
A draft is application-visible when validation, submit gating, dirty-leave
logic, or another view needs it. Store it at an app-db address, usually through
re-frame.hicasso.forms.
The forms module packages draft, baseline, touched state, validation gates, and mutation status around an address you supply (Forms). For a smaller concern, ordinary events and an app-db slice are enough:
The important part is that the draft has one address and no local duplicate.
3. Host-private mechanics: native state¶
Some state exists only to operate a widget:
- measured geometry
- an in-flight drag position
- composition buffers
- internal focus mechanics
- a chart or map SDK handle
This state may update every pointer move or animation frame, and nothing outside the widget needs it. Keep it inside a named native component or a declared host (The native tier, Interop).
The rule at the edge is: motion stays inside; meaning leaves as one event.
(ns app.board.drag
(:require ["react" :as react]
[re-frame.hicasso :as h]
[re-frame.hicasso.native :as n]))
(n/defcomponent drag-surface
[^js props]
(let [[xy set-xy] (react/useState nil)]
(n/$ :div
{:class "card"
:style (when xy
#js {:translate (str (aget xy 0) "px "
(aget xy 1) "px")})
:on-pointer-move
(fn [e]
(when (pos? (.-buttons e))
(set-xy #js [(.-clientX e) (.-clientY e)])))
:on-pointer-up
(fn [_]
(when xy
((.-onDrop props)
(js/Math.round (/ (aget xy 0) 240))))
(set-xy nil))}
(.-label props))))
(h/defview board-card [{:keys [id]}]
(let [title (h/sub [:card/title id])]
(n/$ drag-surface
{:label title
:on-drop (h/event [col] [:card/dropped id col])})))
Pointer movement remains local React state. The completed drop is an application event, so it enters app-db once.
Hooks belong in the separately defined native component. A defview body may
branch and loop dynamically, so putting hooks there makes hook order depend on
data and moves the body outside Hicasso's headless model.
4. Browser-owned state¶
Sometimes the platform already owns the fact. Do not mirror it in app-db unless the application needs a semantic copy.
CSS should own hover, focus-visible, active state, :has(), and ordinary
<details> disclosure.
Uncontrolled inputs may own scratch text through :default-value, with a
commit on blur. The tradeoff is explicit: app-db, tests, and tools cannot see
mid-edit text (Controlled inputs).
Platform controls may own a presentational toggle, such as a native
popover triggered by :popovertarget (Overlays and focus).
DOM ownership is a local design choice, not a hidden replacement for application state. When validation, another view, routing, or testing needs the fact, move it to app-db.
5. Exit retention: pixels that outlive data¶
App-db records what is true. A dismissed toast should leave app-db immediately, but its DOM node may need a short exit animation. That gap is not ephemeral application state — it is paint retention.
Use the optional re-frame.hicasso.motion module
and motion/presence. That chapter owns the API, phase markers
(::h/mounting / ::h/unmounting), the view :rf/phase prop, SSR behaviour,
and accessibility attributes for exiting nodes.
Common state and its owner¶
| State | Owner | Reason |
|---|---|---|
| Dropdown open | App-db; the overlay module reconciles the platform to it | It changes what the user can do, and tests and Xray need it |
| Field draft | App-db through the forms module | Validation, submit gating, dirty-leave, and replay read it |
| Drag position during a drag | Native host state | High-rate mechanics; dispatch the completed drop once |
| Scroll offset | DOM; routing restores it per route | Do not re-render for every pixel. Commit meaningful thresholds as events when needed |
| Animation / exit retention | CSS for animation; motion/presence for exit retention; host state for rAF mechanics |
App-db records truth, not what is still painted |
| Focus | Browser focus, changed through one-shot focus actions | A mirrored “focused element” value drifts and would update on every Tab |
| Selected tab | App-db, or routing when it should survive reload | Other views, tests, or deep links care |
| WebGL context or SDK handle | Declared host or native component | It is an object identity with an attach/teardown lifecycle, not application data |
Choose a stable instance address¶
Application-visible and form state need an instance key. Hicasso does not
invent one. React's useId is unsuitable because it is tied to render order
and does not provide a durable app-db address.
Use authored data: a keyword, string, number, or flat vector of those values.
- Start with a domain id. Qualify ids when different entity types can
collide:
[:order/id 42]and[:invoice/id 42]. - Key placement state by placement and value state by entity. Two panes may share one order draft while keeping separate expanded/collapsed state.
- Extend a parent key for nested instances.
[panel-id :filter]is often enough. - Apply the same stability test as a React
:key. It must be derived from data, stable across renders, unique in its scope, and deterministic under SSR.
There is no :on-mount¶
Hicasso has no :on-mount or :on-unmount. The job you are trying to perform
already has a more specific owner:
| Job | Use |
|---|---|
| Load data for a screen | Route :resources, or an [:rf.resource/ensure …] from the event that decides the data is wanted (Routing, Async resources) |
| Run startup work once | :initial-events, before first paint (Installation) |
| Animate an entrance or exit | CSS or motion/presence |
| Attach to a DOM node or SDK | A callback ref or declared host (Interop) |
When app-db is the wrong place¶
Do not put a fact in app-db when:
- the browser already owns it, such as hover, focus, or raw scroll position;
- it changes too quickly to be a useful application event, such as pointer movement or per-frame geometry;
- nobody outside one widget can observe or act on it, such as an SDK handle.
Everything else is application state and should have one app-db address.
;; Don't: this atom is recreated whenever the body runs, and Hicasso does not
;; track it as reactive state.
(h/defview broken-panel [{:keys [id title]}]
(let [expanded? (atom false)]
[:section
[:h3 {:on-click (fn [_] (swap! expanded? not))} title]
(when @expanded?
[panel-body {:id id}])]))
;; Don't: one event, subscription pass, and paint for every pointer move.
:on-pointer-move
(h/event [e] [:card/drag-moved id (.-clientX e) (.-clientY e)])
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
You are reaching for useState or r/atom to hold “is this open?” |
Application-visible state is moving into a private store | Give it an app-db address, or use the overlay module's reconciled open flag |
| A view-local atom resets or never repaints the view | The body can re-run or be abandoned, and Hicasso does not subscribe to the atom | Move the fact to app-db; move genuine widget mechanics into a native component |
You are looking for :on-mount, componentDidMount, or a mount effect |
Hicasso has no generic lifecycle hook | Identify the job and use the owner in the table above |
| Every panel opens at once | All instances share one address | Include a stable instance key in the address |
| Typing or dragging lags and Xray shows an event per pointer move | High-rate mechanics were routed through app-db | Keep pointer mechanics inside the host and dispatch only the semantic result |
| A dismissed item vanishes before its CSS exit finishes | Exit retention was treated as app-db state, or Presence was not used | See Motion and presence |
app-db accumulates many :ui entries |
Application-visible UI state is correctly stored there | Namespace the slice and exclude it from persistence when appropriate |
| A test simulates clicks only to open a dropdown | The open flag is data | Seed the address directly in the test (Testing) |
Coming from Reagent
r/atom solved a view-local reactivity problem that Hicasso does not
create. Put semantic state at addresses, drafts in the forms model,
mechanics in hosts, browser-owned facts in the DOM, and exit retention in
Motion and presence.