app-db: one map, one write path¶
The introduction showed how re-frame2 computes. This page follows the application data through that loop: where it lives and how it changes through events.
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¶
In the intro, the frame's app-db started as {} and the display showed 0 only because the subscription fell back when :value was missing. After the first click, app-db became {:value 1}.
This page makes the initial value explicit through an :initialise event, then adds a second fact — :step-size — in the same map. Still 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]]
The 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 event's 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}
The seed is not a lone {:value 0} that later grows a second key by accident. 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 that creates and scopes the frame and runs the seed, and a view. (The playground mounts the trailing hiccup; 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: a domain handler that returns the first map, then any follow-ups you need:
(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. If you store them, they can drift from the items. 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.