HTTP from a machine¶
An article request may belong to a screen's loading state: leaving that state
should stop the request. Spawn :rf.http/managed from the state instead of
issuing a separate effect and arranging cancellation yourself.
The machine lives in an app namespace that requires both artefacts:
;; src/app/article_request.cljc
(ns app.article-request
(:require [re-frame.core :as rf]
[re-frame.machines]
[re-frame.http.managed]))
The cell registers the machine and runs it against a stubbed article URL.
Load sends the machine [:article/load]:
(require '[re-frame.core :as rf]
'[re-frame.http.managed]
'[re-frame.http.stub-transport :as stub-transport])
;; A fake backend answers this frame's requests, the child's included.
;; A real app leaves it out.
(stub-transport/install-transport! :app/articles
(stub-transport/route-transport
{[:get "/api/articles/intro"] {:body {:slug "intro" :title "Welcome"}}}))
(rf/reg-machine :article/request
{:initial :idle
:data {:article nil}
:states
{:idle {:on {:article/load :loading}}
:loading
{:spawn {:machine-id :rf.http/managed
:data {:request {:url "/api/articles/intro"}
:decode :json}
:on-done {:target :ready
:action (fn [{data :data [_ _ completion] :event}]
{:data (assoc data :article (:result completion))})}
:on-error :failed}
:on {:article/cancel :idle}}
:ready {:on {:article/load :loading}}
:failed {:on {:article/load :loading}}}})
(rf/reg-view article-request-view []
(let [{:keys [state data]} (or @(subscribe [:rf/machine :article/request])
{:state :idle :data {:article nil}})]
[:div
[:button {:on-click #(dispatch [:article/request [:article/load]])} "Load"]
[:p "state: " (str state)]
[:p "article: " (pr-str (:article data))]]))
[rf/frame-root {:id :app/articles}
[article-request-view]]
Add day8/re-frame2-machines and day8/re-frame2-http to the app's dependencies.
Requiring both namespaces registers the HTTP child machine. The parent's
:spawn starts it on entry to
:loading; its :on-done receives the child's completion, whose :result is
the decoded article. Its action stores the article in the parent's :data and
enters :ready. Failure reaches :on-error, which also handles a child that
fails to start.
The stub transport answers at once, so the cell moves from :idle straight to :ready.
In the tutorial's mounted frame, start the machine from the REPL with:
Read its state and data through the ordinary machine subscription,
[:rf/machine :article/request]. The result stays in machine :data; it does
not update the tutorial's [:article] app-db slice automatically.
The child addresses its own reply¶
The child's :data accepts the HTTP args map, including decoding, retries and
per-attempt timeout. Leave out :reply-to, :on-success and :on-failure: the
child routes its own reply and reports its decoded value or failure to the parent.
It rejects those keys even when their value is nil, with
:rf.error/http-bad-reply-target; the parent's :spawn :on-error handles that
startup failure.
A single child completes like any child machine, rather than with the
event-effect form's full reply map: the spawn's :on-done reads the decoded
value as the completion's :result, and its :on-error reads the
classified failure.
Cancellation¶
While the request is loading, cancel it with:
Any transition out of :loading destroys its child and aborts the child's HTTP
work. That includes cancel, successful completion, failure and parent teardown.
The cancelled child's reply is suppressed; the transition to :idle clears the
loading state without waiting for another event.
This also applies when you issue an HTTP effect directly from a spawned actor.
A reply addressed back to the destroyed actor is suppressed. A reply addressed
to an ordinary event in the surviving frame receives :status :cancelled with
:reason :actor-destroyed under :error. Frame destruction or epoch restore
suppresses either address. Put cleanup that must run during teardown in the
actor's exit action.
Load the article and comments together¶
When the screen needs two independent responses before it can render, replace
:loading above with a :spawn-all state. Each child handles one HTTP request;
the parent becomes ready after both succeed:
;; cf. examples/patterns/boot/boot.cljs — child result folds and a :join :all
:loading
{:spawn-all
{:children
[{:id :article
:machine-id :rf.http/managed
:data {:request {:url "/api/articles/intro"} :decode :json}
:on-done (fn [{:keys [data result]}] (assoc data :article result))}
{:id :comments
:machine-id :rf.http/managed
:data {:request {:url "/api/articles/intro/comments"} :decode :json}
:on-done (fn [{:keys [data result]}] (assoc data :comments result))}]
:join :all
:on-all-complete [:article/ready]
:on-any-failed [:article/load-failed]}
:on {:article/ready :ready
:article/load-failed :failed
:article/cancel :idle}}
Each :on-done folds its child's result into the parent's :data. Under
:spawn-all, HTTP children finish through the join; they do not send the
single-child :succeeded or :failed events. One failure moves the parent to
:failed and cancels any remaining child. Leaving :loading also cancels them.
The join guide covers other join policies.
When to keep event handlers¶
Use the effect form for a request with a normal receiving event. If one request needs another's result, its success handler can issue the next request. Introduce a machine when explicit states, branching or cancellation make that sequence easier to follow. Use resources when several screens need the same cached article.