Skip to content

Instantly share code, notes, and snippets.

@piotr-yuxuan
Last active May 23, 2017 13:32
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 piotr-yuxuan/f03dba032e53252d4cc9344ee2044152 to your computer and use it in GitHub Desktop.
Save piotr-yuxuan/f03dba032e53252d4cc9344ee2044152 to your computer and use it in GitHub Desktop.
Mere attempt for an idiomatic way to populate app-db with data from the outter world in re-frame

Mere attempt for an idiomatic way to populate app-db with data from the outter world in re-frame

This is a short code snippet from a pet project I used to have fun with five or six months ago. It may not work as I just pasted code from it but I hope it shows the main ideas.

What is this gist for

This gist takes as granted you know the documentation of re-frame. It give an example to this part of the documentation: subscribing to external data Poke me if you want help about it.

What this gist contains

It suggests an idiomatic way to retrieve data from the outter world within re-frame mindset. The idea is to be pure and enforces the definition of app-db: single source of truth. A Reagent component should trust it and shouldn't be able to question it.

A component only subscribes to data in the per-definition true app-db. Subscriptions and events take care of issuing requests to data from the outter world and inserting it into the db. Such asynchronous insertion results in a update of any component subscribing to this piece of data.

In addition to that, this gist shows some uncommon usages of re-frame interceptors.

In further addition to that, this gist shows a way to debounce data requests to avoid sinking an external database with useless yet costly queries. Warning: debouncing is different from throttling ;-)

Why it's interesting

For whom cares about purity:

  • Component are pure functions. This way to do it ensures it.
  • Use Effectful handlers to keep your event handlers pure.
  • Purer code is easier to understand.

As a reward for being pure, you can use Figwheel in a much better way

  • Bad way to use Figwheel: each time you modify the code of your unpure component, Figwheel reloads it. Your component issues data requests so data it relies on are wiped and you loose its state. It takes only 10 seconds for you to get back in the same state but you have to do it every single times you change the code of your component. You end up loosing a lot of time.

  • Natural way to use Figwheel: each time you modify the code of a pure component, Figwheel reloads it. Your component subscribes to data which already are in app-db so its state is preserved. Perhaps (as shown herein this code) you want to refresh dat in background: this is not a problem since data value itself won't change. As re-frame relies on data value to reload subscription, if data subscribed to doesn't take a new value, no reload will happen.

Further work

I'll be very happy to be stumbled upon for more details. However, I no longer want to only work about re-frame and you have to give me time to finish and publish this project as a real POC ;-)

(ns your-project.debounce-fx
(:require
[re-frame.router :as router]
[re-frame.fx :refer [register]]
[re-frame.db :refer [app-db]]
[re-frame.interceptor :refer [->interceptor]]
[re-frame.interop :refer [set-timeout!]]
[re-frame.registrar :refer [get-handler clear-handlers register-handler]]
[re-frame.loggers :refer [console]]))
(def debounced-events (atom {}))
#?(:cljs (defn cancel-timeout [id]
(js/clearTimeout (:timeout (@debounced-events id)))
(swap! debounced-events dissoc id)))
#?(:cljs (register
:dispatch-debounce
(fn [dispatches]
(let [dispatches (if (sequential? dispatches) dispatches [dispatches])]
(doseq [{:keys [id action dispatch timeout]
:or {action :dispatch}}
dispatches]
(case action
:dispatch (do
(cancel-timeout id)
(swap! debounced-events assoc id
{:timeout (js/setTimeout (fn []
(swap! debounced-events dissoc id)
(router/dispatch dispatch))
timeout)
:dispatch dispatch}))
:cancel (cancel-timeout id)
:flush (let [ev (get-in @debounced-events [id :dispatch])]
(cancel-timeout id)
(router/dispatch ev))
(console :warn "re-frame: ignoring bad :dispatch-debounce action:" action "id:" id)))))))
(ns your-project.events
(:require [re-frame.core :refer [reg-event-db reg-event-ctx reg-event-fx ->interceptor]]
[day8.re-frame.http-fx]
[your-project.utils :refer [format-path http-request-attributes]]
[ajax.core :refer [json-response-format]])
(:require-macros [adzerk.env :as env]))
(env/def EXAMPLE_GITHUB_TOKEN :required)
(defn github-api
[{:keys [path] :as attrs}]
(merge attrs
{:uri (str "https://api.github.com" (format-path path))
:method :get}))
(reg-event-db
:store-retrieved-commit
(fn [db [_ {:keys [sha] :as commit}]]
(assoc-in db [:commits-index sha] commit)))
(reg-event-ctx
:github-api
[(http-request-attributes github-api)]
(fn [cofx]
;; Makes explicit the use of a coeffect to produce an effect.
(assoc-in cofx [:effects :http-xhrio]
(-> cofx :coeffects :event))))
(reg-event-db
:store-retrieved-commits
(fn [db [_ commits]]
(assoc db :commits-index (reduce #(assoc %1 (:sha %2) %2)
{}
commits))))
(reg-event-db
:store-commit
(fn [db [_ & args]]
(println :store-commit (-> args first :sha))
(assoc-in db [:commits-index (-> args first :sha)] (-> args first))))
(ns your-project.subs
(:require-macros [reagent.ratom :refer [reaction]])
(:require [re-frame.core :as re-frame :refer [->interceptor reg-fx reg-event-db reg-event-fx reg-event-ctx subscribe reg-sub dispatch]]
[reagent.ratom :refer [make-reaction]]
[re-frame.std-interceptors :refer [trim-v]]
[goog.dom :refer [getViewportSize isNodeLike]]
[your-project.debounce-fx]
[reagent.core :as reagent]))
(re-frame/reg-sub-raw
:cached-commits
(fn [app-db [_ long-sha]]
(dispatch [:dispatch-debounce {:id (keyword "cached-commit-" long-sha)
:timeout 1
:dispatch [:github-api {:path [:repos :torvalds :linux :commits long-sha]
:on-success [:store-retrieved-commit]}]}])
(make-reaction
(fn [_] (get-in @app-db [:commits-index long-sha] []))
:on-dispose (fn [_]
(comment "Unclear semantic, I don't use it right now")
(.log js/console :on-dispose))
:on-set (fn [_]
(comment "Unclear semantic, I don't use it right now")
(.log js/console :on-set)))))
(ns your-project.utils
(:require [re-frame.core :refer [->interceptor reg-event-db reg-event-fx reg-event-ctx]]
[day8.re-frame.http-fx]
[ajax.core :refer [json-response-format]]))
(defn format-path
[path]
(let [to-string-fn #(cond (keyword? %) name :else str)
path-delimiter "/"
path-vec->str (fn [path] (reduce #(str %1 path-delimiter ((to-string-fn %2) %2)) "" path))
format-path-fn #(if (coll? %) path-vec->str identity)]
((format-path-fn path) path)))
(def ^:private default-http-request-attributes
{:params {}
:timeout 8000
:response-format (json-response-format {:keywords? true})
:on-success [:identity]
:on-failure [:identity]})
(def ^:private http-request-keys
'(:uri :method :params :timeout :response-format :on-success :on-failure))
(defn http-request-attributes
[request-builder]
(->interceptor
:before (fn [context]
(let [format-params (fn [event]
(-> default-http-request-attributes
(merge (request-builder (rest event)))
(select-keys http-request-keys)))]
(update-in context [:coeffects :event] format-params)))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment