Skip to content

Load data for a route

Load an article when its URL opens, show loading and failure states, and discard late replies after the reader leaves. Declare the read on the route with :resources; the view reads the cache and never starts a request.

Extend the tutorial's app.core namespace with the Resources and managed HTTP packages, day8/re-frame2-resources and day8/re-frame2-http:

(ns app.core
  (:require [re-frame.core :as rf]
            [re-frame.routing]
            [re-frame.resources]
            [re-frame.http.managed]
            #?(:cljs [re-frame.adapter.reagent :as reagent-adapter])))

(rf/reg-event :article/remember
  (fn [{:keys [db] rt :rf.db/runtime} _]
    {:db (assoc db :article/last-read
                (get-in rt [:rf.runtime/routing :current :params :slug]))}))

;; cf. examples/capabilities/resources/resources/core.cljs
(rf/reg-resource :article/detail
  {:params-schema [:map [:slug :string]]
   :scope :rf.scope/global}
  (fn [{:keys [slug]} _ctx]
    {:request {:method :get :url (str "/api/articles/" slug)}
     :decode :json}))

(rf/reg-route :app/article
  {:parent :app/articles
   :params [:map [:slug :string]]
   :on-match [[:article/remember]]
   :resources [{:resource :article/detail
                :params (fn [route] {:slug (get-in route [:params :slug])})
                :blocking? true}]}
  "/articles/:slug")

This replaces the tutorial's :app/article registration and its local :on-match loader. The new activation event remembers the slug for the Continue reading button, while resources load the article. Register the resource, route and events before creating the frame: a URL-bound frame loads its first route during creation. Here the API returns an article JSON object with a title field. :rf.scope/global means these articles are public and the same for every reader; for per-user data, use a scope resolver.

Render the result

Replace article-page with a view that reads the same resource and params:

(rf/reg-view article-page []
  (let [{:keys [slug]} @(subscribe [:rf.route/params])
        article @(subscribe [:rf/resource {:resource :article/detail
                                           :params {:slug slug}}])]
    (cond
      (:has-data? article) [:h1 (:title (:data article))]
      (:error article)     [:p.error "Could not load the article."]
      :else                [:p "Loading article…"])))

Opening /articles/intro starts the request. The view first shows a loading message, then the article title or an error. Navigating to another article changes the subscribed cache entry. Leaving releases the route's ownership; a superseded request cannot overwrite the new page. The resource cache manages reuse and freshness.

reg-route replaces the whole metadata map. Keep :parent, the param schema and any guards when changing the loader. Use :on-match alongside resources for work such as recording a page visit; its events do not control readiness.

Show progress across pages

:blocking? true includes this read in the route's readiness. It keeps :rf.route/transition at :loading during a first load without data and gives server rendering a read to wait for. On the client, the URL and route commit immediately, so the loading view can render.

Render this view once in the root to report progress across routes:

(rf/reg-view route-status []
  (case @(subscribe [:rf.route/transition])
    :loading [:p {:role "status"} "Loading page…"]
    :error [:p.error "Could not load this page."]
    nil))

The structured failure is available through [:rf.route/error]. A failed blocking first load reports :rf.error/resource-route-blocking; a failed plan reports :rf.error/resource-route-plan. Once all blocking reads have data, the transition is :idle. The model explains the readiness rules.

A cached article stays visible during a refresh, and a failed refresh stays on the resource rather than the route. Non-blocking reads, prefetches and :on-match never change the route transition. A resource declared with :blocking? false starts on entry too; give its view its own loading state.

Try it

This cell runs the resource, the route and both views on an in-memory frame. HTTP stubs stand in for the server, and the frame's :fx-overrides sends its requests to them. Open each article: the stubs answer at once, so the loading state passes too quickly to see. A missing article gets a 404, so the page shows its error, the transition reads :error and the route error is :rf.error/resource-route-blocking. Going back to Intro shows the cached article.

(require '[re-frame.core :as rf]
         '[re-frame.routing]
         '[re-frame.resources]
         '[re-frame.http.managed]
         '[re-frame.http.test-support :as rf.http.test-support])

;; Stand-in for the server: two articles and a 404.
(rf.http.test-support/install-managed-request-stubs!
  {[:get "/api/articles/intro"]   {:reply {:ok {:title "Intro to re-frame2"}}}
   [:get "/api/articles/ssr"]     {:reply {:ok {:title "Server rendering"}}}
   [:get "/api/articles/missing"] {:reply {:failure {:kind :rf.http/http-4xx :status 404}}}})

(rf/reg-event :article/remember
  (fn [{:keys [db] rt :rf.db/runtime} _]
    {:db (assoc db :article/last-read
                (get-in rt [:rf.runtime/routing :current :params :slug]))}))

(rf/reg-resource :article/detail
  {:params-schema [:map [:slug :string]]
   :scope :rf.scope/global}
  (fn [{:keys [slug]} _ctx]
    {:request {:method :get :url (str "/api/articles/" slug)}
     :decode :json}))

(rf/reg-route :app/articles {} "/articles")
(rf/reg-route :app/article
  {:parent :app/articles
   :params [:map [:slug :string]]
   :on-match [[:article/remember]]
   :resources [{:resource :article/detail
                :params (fn [route] {:slug (get-in route [:params :slug])})
                :blocking? true}]}
  "/articles/:slug")

(rf/reg-view article-page []
  (let [{:keys [slug]} @(subscribe [:rf.route/params])
        article @(subscribe [:rf/resource {:resource :article/detail
                                           :params {:slug slug}}])]
    (cond
      (:has-data? article) [:h1 (:title (:data article))]
      (:error article)     [:p.error "Could not load the article."]
      :else                [:p "Loading article…"])))

(rf/reg-view route-status []
  (case @(subscribe [:rf.route/transition])
    :loading [:p {:role "status"} "Loading page…"]
    :error [:p.error "Could not load this page."]
    nil))

(rf/reg-view article-reader []
  [:div
   [:nav [rf/route-link {:to :app/article :params {:slug "intro"}} "Intro"] " · "
         [rf/route-link {:to :app/article :params {:slug "ssr"}} "Server rendering"] " · "
         [rf/route-link {:to :app/article :params {:slug "missing"}} "A missing article"]]
   [route-status]
   [article-page]
   [:p [:code (pr-str {:transition @(subscribe [:rf.route/transition])
                       :error      (:rf.error/id @(subscribe [:rf.route/error]))})]]])

[rf/frame-root {:id             :app
                :fx-overrides   {:rf.http/managed :rf.http/managed-test-stub}
                :initial-events [[:rf.route/navigate {:to     :app/article
                                                      :params {:slug "intro"}}]]}
 [article-reader]]

Share a parent's data

Put data needed by a section's shell on its parent route. For example, register the public tag list and add it to the articles route:

(rf/reg-resource :article/tags
  {:params-schema [:map]
   :scope :rf.scope/global}
  (fn [_params _ctx]
    {:request {:method :get :url "/api/tags"}
     :decode :json}))

(rf/reg-route :app/articles
  {:query [:map [:tag {:optional true} :string]]
   :resources [{:resource :article/tags :blocking? true}]}
  "/articles")

The article route already names :app/articles as its :parent, so entry loads the tag list and article detail together. An identical requirement contributed by several routes is fetched once. Only :resources compose this way; guards and :on-match remain specific to each route. The root view still renders the layout.

Troubleshooting

Symptom Cause Fix
reg-route rejects :resources The Resources package is not loaded Require re-frame.resources before registering routes
First page reports :rf.error/resource-route-plan A resource or parent route is not registered, or its params or scope cannot resolve Read :rf.route/error; register dependencies before creating the frame
First request throws :rf.error/http-artefact-missing The managed HTTP transport is not loaded Require re-frame.http.managed
Data loads but the article view stays empty Route and view use different resource params Build the view's params from the same route values
An :on-match failure does not show in the route's error view Activation events report through the ordinary event error channel Use resources for page data; handle other event failures where they occur

Advanced

Warm the destination before a click

[rf/route-link {:to :app/article :params {:slug "intro"} :prefetch :intent}
 "Read intro"]

Hover, focus or touch starts the destination's resources, including its parents'. The click reuses the data or in-flight request. Prefetch changes no route state and runs no guards or :on-match; it cannot grant entry to a protected route. Omit :prefetch to turn it off; :intent is its only mode.

Reload the plan after an identity change

After restoring a session or switching tenant, a scoped resource may select a new cache entry while the route stays the same. Navigating to the identical URL does nothing. Clear the old scope, commit the new identity, then dispatch:

;; From the identity-change event's :fx, or a view's injected dispatch.
[:rf.route/replan-resources {:cause [:session-restore]}]

The active route's plan runs again against the current app-db. Reusable data and in-flight reads are kept, missing reads are loaded, and dropped reads are released. No guards, :on-match, URL or scroll work runs. A failed replan releases the old plan and reports :rf.error/resource-route-plan on the route. The exact request forms are in the routing reference.