Resources & server state¶
Most of what a real app shows is not its state — it is the server's, borrowed and cached: the article you are reading, the feed you are scrolling, the profile you just edited. Manage that by hand and you rebuild the same machinery everywhere — fetch on mount, store the result, track loading and error, dedupe in-flight calls, decide staleness, refetch after a write, and not leak one user's data into another's session.
Resources make server state declarative. You register a cached read
(reg-resource) and a write
(reg-mutation). The framework owns the cache, dedupe,
invalidation, and a fail-closed :scope
leak boundary. Views read — they never fetch.
(:require [re-frame.core :as rf]
[re-frame.resources]
[re-frame.http.managed])
(rf/reg-resource :article
{:params-schema [:map [:slug :string]]
:scope :rf.scope/global}
(fn [{:keys [slug]} _ctx]
{:request {:method :get :url (str "/api/articles/" slug)}
:decode :json}))
;; a view only reads — a subscription NEVER fetches
@(rf/subscribe [:rf/resource {:resource :article :params {:slug "hello"}}])
;; => {:status :idle …} — registered, but nothing has caused a load yet
;; a route or an event is the cause; here, a one-shot ownerless ensure
;; (supply a :cause, not an :owner — nothing pins the entry alive)
(rf/dispatch [:rf.resource/ensure {:resource :article
:params {:slug "hello"}
:cause [:event :article/opened]}])
;; now the same passive read progresses
@(rf/subscribe [:rf/resource {:resource :article :params {:slug "hello"}}])
;; => {:status :loading …} then {:status :loaded :data …}
Prerequisites. The Core introduction. Resources plug into
events, app-db, and effects; they do not replace them. The transport underneath is
managed HTTP (:rf.http/managed).
Three lanes¶
Hold this split and the API stays clear:
| Lane | Job | Spelling |
|---|---|---|
| Register | Declare the handler once | (rf/reg-resource …) / (rf/reg-mutation …) |
| Cause | Make a fetch or write happen | route :resources, [:rf.resource/ensure …], [:rf.mutation/execute …] |
| Project | Read state into a view | @(subscribe [:rf/resource …]) — never fetches |
A subscription that finds no entry stays :idle until something causes a load.
The model page ends with a complete register + route + view skeleton.
When not to use resources¶
| Situation | Prefer |
|---|---|
| One or two uncached requests | Managed HTTP + a small app-db slice |
| Pure client state (UI flags, form drafts) | app-db + events |
| Named lifecycle stages (login, websocket) | machines |
| No server yet | Tutorial Part 1 only |
Reach for resources when cached server reads start multiplying — not for a local counter. Where should this value live? has the full decision table.