app-db: one map, one write path¶
The introduction walked the event pipeline; events named the vocabulary. This page follows application data: where it lives, and how it changes.
app-db is the application state for one frame: one immutable Clojure map. Handlers return a description of the next map; the event pipeline is what actually writes.
One map. One write path. Events in, new map out.
Almost everything else in re-frame2 stays simple because of that.
A complete live counter¶
The intro counter seeded :value through its :initialise event; the
(:value db 0) in its subscription was a defensive display fallback, not the
initializer. Here we reuse that pattern and add a second fact — :step-size — so
both begin explicit in one store. 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 _event]
{:db {:value 0 :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:"]
[: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]]}
[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.
No second store. The events produce a sequence of complete map values:
[:initialise]
;; => {:value 0 :step-size 1}
[:step-size/set {:step-size 10}]
;; => {:value 0 :step-size 10}
[:inc]
;; => {:value 10 :step-size 10}
Both facts begin in the initial map; later events return replacements for that whole value. That is a complete live counter: registrations, a frame-root, and a view. (A real app still needs boot wiring — counter example or Boot and mount an app.) The rest of the guide grows this same counter one concept at a time.
One map, one write path¶
Everything your app owns as state between events sits in one map — ordinary nested data, with no framework-imposed shape:
Exactly one normal write path: dispatch an event; the
handler returns an effect map
that may include :db; the runtime commits that new map
atomically. Handlers compute a proposed next value — they do not mutate the old one.
(rf/reg-event :cart/add
(fn [{:keys [db]} [_ {:keys [item]}]]
{:db (update-in db [:cart :items] conj item)}))
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.
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.
Gotcha — {:db nil}
app-db is always a map. An accidental {:db nil} is coerced to {} with a dev
warning (:rf.warning/db-nil-coerced). To clear state on purpose, write
{:db {}}.
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), not a spread-operator discipline.
From re-frame v1
One app-db, handlers return a new value — same spirit. What changed: app-db is
only your application data. Framework bookkeeping lives next door in
runtime-db, not under a :rf/runtime
root inside your map.
Initial state is an event¶
A frame's event fold starts with app-db = {}. There is no :db config slot to seed
it. Seeding is itself an event — the same pipeline as every later change — listed as
:initial-events on the frame (or frame-root).
The counter already did this with :initialise. A larger app is the same idea:
(rf/reg-event :initialise
(fn [_world _event]
{:db {:session nil
:ui {:route :home}}}))
[rf/frame-root {:id :app
:initial-events [[:initialise]
[:session/restore]]}
[root-view]]
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 — no seeding side channel. Prefer a named map payload when an initialise
event carries options: [:initialise {:user-id 42}] with [_ {:keys [user-id]}].
Frames covers the rest of the registration grammar.
From re-frame v1
:initial-db and :on-create are gone. Seed with :initial-events and your own
initialise event (or several), not a config map.
Shape the map around the domain¶
Use ordinary maps and vectors. Prefer stable domain paths (:session, :articles,
:ui) over scattering presentation flags next to every fact. Views stay thin; they
read what they need through subscriptions.
Store facts. Derive conclusions.¶
Put facts in app-db. Let subscriptions derive view-facing conclusions (later, flows cover derivations you intentionally materialise back into app-db).
Good:
Poor:
total and empty? are conclusions. Store them next to the items and they will
drift — two copies of one truth is two chances to disagree. Derive them so there is
only one truth to update. That is the next page's job.
Yours, and the framework's next door¶
For state your application owns between events, app-db is the write target. A running
frame also holds runtime-db — framework bookkeeping
(machine snapshots, route, resource cache, …) under reserved :rf.runtime/* keys.
Two partitions: app-db is yours; runtime-db is the
framework's (read it via its subscriptions; influence it by dispatching its events —
never forge it by hand in app-db). An ordinary :db effect cannot wipe a machine
snapshot. Frames goes deeper.
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
| State "vanished" after a handler | Accidental {:db nil} |
Write {:db {}} to clear; watch for :rf.warning/db-nil-coerced |
| 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 before first paint | No seed event | List an initialise event in :initial-events |
Framework snapshot gone after your :db |
You tried to own runtime keys in app-db | Leave runtime-db alone; dispatch the subsystem's events |
With this page you can seed a frame, keep facts in one map, write only through events, and leave conclusions out of app-db. The left nav continues into how those conclusions are named and cached.