Skip to content

Instantly share code, notes, and snippets.

@yannvanhalewyn
Last active April 6, 2022 14:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yannvanhalewyn/18164763b8e88c73cfcadb497cde1091 to your computer and use it in GitHub Desktop.
Save yannvanhalewyn/18164763b8e88c73cfcadb497cde1091 to your computer and use it in GitHub Desktop.
Subscribing to remote data with re-frame
(ns my-app.articles.db
(:require [my-app.http :as http]))
(http/reg-sub-remote
::articles
(fn []
{::http/path "/articles"
::http/method :post
::http/params {}
:db/path [:db/articles]
;; Will dissoc data when unsubscribing (i.e leaving the page) making it
;; refetch when coming back.
:db/dispose true
;; Can be extended with any arbitrary logic such as polling / refetching,
;; timeouts, pubsub, whatever. Some examples:
::http/ttl (t/hours 1) ;; Useful for configuration endpoints. Make it
;; refetch asynchronously if subscription is called
;; again later and just replace the data when it comes in.
;; Start an interval before the make-reaction and dispose of it in on-dispose
:poll/interval (t/seconds 2)
:poll/refresh? (fn [current-data last-status]
(should-refresh? current-data last-status))}))
(ns my-app.http
(:require [re-frame.core :as rf]
[reagent.ratom :as ratom]
[cljs.core.async :refer [<! go]]
[cljs.core.async.interop :refer [<p!] :include-macros true]))
(defn- mark-pending [db query-id]
(assoc-in db [:db/http ::requests query-id] {::status ::pending}))
(defn- mark-success [db query-id]
(assoc-in db [:db/http ::requests query-id] {::status ::success}))
(defn- mark-failed [db query-id body]
(assoc-in db [:db/http ::requests query-id] {::status ::failed ::body body}))
(defn- get-status [db query-id]
(get-in db [:db/http ::requests query-id]))
(defn pending? [{::keys [status]}]
(= status ::pending))
(rf/reg-fx
::http
(fn [{::keys [path params method on-success on-failure]}]
;; Use <!p if http-client/get returns a promise, wrap in try catch if promise
;; can get rejected
(go (let [result (<!p (http-client/request
#js {:path path :method method :params params}))]
(if (http-client/success? result)
(rf/dispatch (conj on-success result))
(rf/dispatch (conj on-failure result)))))))
(rf/reg-event-fx
::read
(fn [{:keys [db]} [_ query-id {::keys [path method params] :as config}]]
{:db (mark-pending db query-id)
;; Remember we don't want side-effects in event-handlers. Dispatch this to a
;; generic reusable ::http fx
::http {::path path
::params params
::method (or method :get)
::on-success [::success query-id config]
::on-failure [::failed query-id config]}}))
(rf/reg-event-db
::success
(fn [db [_ query-id {:db/keys [path]} response]]
(-> db
(mark-success query-id)
(assoc-in path (:body response)))))
(rf/reg-event-db
::failed
(fn [db [_ query-id _config response]]
(mark-failed db query-id (:body response))))
(defn reg-sub-remote [query-id config-fn]
(rf/reg-sub-raw
query-id
(fn [db sub-vec]
(let [config (config-fn sub-vec)
reaction (ratom/make-reaction
(fn []
;; Returns a tuple of [data status] as a reaction to db
[(get-in @db (:db/path config))
(get-status @db sub-vec)])
:on-dispose
;; Cleanup any polling if needed here
(when (:db/dispose config)
;; This should dissoc any data at :db/path
#(rf/dispatch [:db/dispose config])))]
(when (nil? (second @reaction))
(rf/dispatch [::read sub-vec config]))
reaction))))
(ns my-app.articles.list
(:require [my-app.http :as http]
[my-app.articles.db :as articles.db]
[re-frame.core :as rf]))
(defn- component []
(let [[articles status] @(rf/subscribe [::articles.db/articles])]
(if (http/pending? status)
[:div "Loading.."]
[article-post aticles])))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment