app-db: one map, one write path¶
Your application's state has to live somewhere, and something has to change it. In re-frame2 it lives in app-db: one immutable Clojure map per frame. Event handlers return the next map; the event pipeline writes it.
A complete live counter¶
The counter from the Introduction gains a second fact,
:step-size, and a decrement button. :initialise still takes the starting value
and now seeds both facts. Click the buttons (edit the cell and press
Ctrl-Enter / Cmd-Enter if you change the code):
(require '[re-frame.core :as rf])
(rf/reg-event :initialise
(fn [_world [_ start]]
{:db {:value start :step-size 1}}))
(rf/reg-event :step-size/set
(fn [{:keys [db]} [_ {:keys [step-size]}]]
{:db (assoc db :step-size step-size)}))
(rf/reg-event :inc
(fn [{:keys [db]} _]
{:db (update db :value + (:step-size db))}))
(rf/reg-event :dec
(fn [{:keys [db]} _]
{:db (update db :value - (:step-size db))}))
(rf/reg-sub :value (fn [db _] (:value db)))
(rf/reg-sub :step-size (fn [db _] (:step-size db)))
(rf/reg-view stepping-counter []
[:div
[:button {:on-click #(dispatch [:dec])} "−"]
[:span " " @(subscribe [:value]) " "]
[:button {:on-click #(dispatch [:inc])} "+"]
[:span {:style {:margin-left "1.5em"}} "step " @(subscribe [:step-size]) ":"]
[:button {:on-click #(dispatch [:step-size/set {:step-size 1}])} "1"]
[:button {:on-click #(dispatch [:step-size/set {:step-size 10}])} "10"]])
[rf/frame-root {:id :app
:initial-events [[:initialise 0]]}
[stepping-counter]]
Handler parameters use destructuring.
{:keys [db]} takes app-db from the world map. [_ {:keys [step-size]}] ignores
the event id and takes :step-size from the payload map.
The events produce a sequence of complete map values:
[:initialise 0]
;; => {:value 0 :step-size 1}
[:step-size/set {:step-size 10}]
;; => {:value 0 :step-size 10}
[:inc]
;; => {:value 10 :step-size 10}
Each event returns a replacement for the whole value. Outside this in-browser environment a real app also needs boot wiring: see the counter example or Boot and mount an app.
The write path¶
Everything your app keeps as state between events sits in one map of ordinary nested data, with no framework-imposed shape. A todo list, which later pages use, might hold:
{:todos {1 {:id 1 :title "Buy milk" :done? false}
2 {:id 2 :title "Walk the dog" :done? true}}
:showing :all}
There is one normal way to change it: dispatch an event; the
handler returns an effect map
that may include :db; the runtime commits that new map
atomically. Handlers compute the next value; they do not mutate the old one.
update-in returns a new map. The runtime later moves the app-db reference from
the old value to the new one in a single commit. (The place is app-db; the value
currently in it is usually bound as db.)
That gives you three useful properties:
- no view sees a half-written state;
- old values can be inspected, diffed, or restored;
- handlers are pure functions you can unit test.
To look at app-db from the REPL, call (rf/app-db-value :app). It returns the
frame's current map, or nil when no frame has that id.
A handler may return no :db key (only :fx, say) and leave app-db alone, or return
the same db object it was handed so the runtime skips a no-op write.
Coming from Redux?
app-db is the single store; a handler is a pure function that returns the next
state as data ({:db …}), and the runtime commits it. No combined reducers, no
prescribed slice shape — one ordinary Clojure map. Immutability is by
construction: update-in cannot mutate, so no spread-operator discipline is needed.
From re-frame v1
One app-db, and handlers return a new value, as in v1. app-db holds only your
application data; framework bookkeeping lives next door in
runtime-db. v1's reg-event-db and
reg-event-fx are one reg-event that always returns an effect map.
Initial state is an event¶
Every frame starts with app-db = {}. There is no config option to seed it; seeding
is itself an event, run through the same pipeline as every later change and listed
under :initial-events on the frame (or frame-root).
The counter already did this with :initialise. A larger app is the same idea, and
can list several events:
(rf/reg-event :todo/initialise
(fn [_world _event]
{:db {:todos {} :showing :all}}))
[rf/frame-root {:id :app
:initial-events [[:todo/initialise]
[:todo/add "Buy milk"]]}
[todo-app]]
Each initial event's pipeline runs synchronously, in order, through its immediate
commit before the next begins. If a handler starts asynchronous work, its reply
arrives later through another event; setup does not wait for the host operation. By
the time setup finishes, app-db includes the immediate :db commits from the initial
events. Prefer a named map payload when an initialise event carries options:
[:initialise {:user-id 42}] with [_ {:keys [user-id]}].
When all you need is a literal starting map, the framework's own :rf/set-db event
does it without a handler of yours:
[rf/frame-root {:id :app :initial-events [[:rf/set-db {:value 0 :step-size 1}]]}
[stepping-counter]]
Frames covers the other frame options.
Shape the map around the domain¶
Use ordinary maps and vectors. Prefer stable domain paths (:todos, :showing)
over scattering presentation flags next to every fact. Views stay thin; they read
what they need through subscriptions. To have the runtime check that shape as it
changes, register a schema for a path
(Validate with schemas).
Store facts, derive conclusions¶
Put facts in app-db. Let subscriptions derive the conclusions views show (flows cover derivations you deliberately materialise back into app-db).
Good:
Poor:
:remaining-count and :all-done? are conclusions. Stored next to the todos, they
have to be updated by every handler that touches a todo, and one that forgets leaves
them wrong. Derive them with a subscription instead.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
State "vanished" after a handler, and :rf.warning/db-nil-coerced is reported |
app-db is always a map, so a returned {:db nil} is coerced to {} |
Return the map you meant; to clear state on purpose, write {:db {}} |
| Two facts disagree | A conclusion was stored next to its facts | Derive in a subscription (or a flow if handlers must read it) |
| Initial UI shows empty values | No seed event | List an initialise event (or [:rf/set-db {…}]) in :initial-events |
Frame creation throws :rf.error/initial-db-retired or :rf.error/on-create-retired |
The frame options carry :initial-db or :on-create |
Seed through :initial-events |
| Machine or route state doesn't reflect what you wrote into app-db | Framework state lives in runtime-db, not app-db | Dispatch the subsystem's events instead |
Advanced¶
Yours, and the framework's next door¶
A running frame also holds runtime-db: framework
bookkeeping (machine snapshots, the current route, the resource cache, …) under
reserved :rf.runtime/* keys. The two partitions
are separate: app-db is yours, and runtime-db is the framework's. Read runtime-db
through the framework's subscriptions and change it by dispatching the framework's
events; don't recreate its keys in app-db. An ordinary :db effect cannot wipe a
machine snapshot. Frames goes deeper.
Keeping secrets out of traces¶
Tools and traces see a copy of app-db. To redact a path in that copy, return a
classification effect beside :db. :sensitive redacts the value and :large
replaces it with a size marker; :clear-sensitive and :clear-large undo them:
(rf/reg-event :auth/init
(fn [{:keys [db]} _]
{:db (assoc db :auth {})
:sensitive [[:auth :token] [:auth :refresh-token]]}))
Your handlers and subscriptions still read the real value. Classifying a path before anything is stored there is fine, so a seed event is the natural place. Keep secrets out of traces covers the details.