Open20

re-frame document 読みながらメモ

しんせいたろうしんせいたろう

reg-event-db のハンドラ関数のイベントIDをアンダースコアで束縛するのが慣例だけど、ちょっと気持ち悪い時に使う trim-v インターセプター

(rf/reg-event-db
 ::todos 
 (fn [db [_ arg]]  ;;この event-id のアンダーバーキモい
   (assoc db :hoge arg)))

上記と同じことができます

(rf/reg-event-db
 ::todos
 [rf/trim-v] ;; trim-v インターセプタを追記すると、アンダーバーを書かなくてもよい
 (fn [db [args]] 
   (assoc db :hoge arg)))

参照:
re-frame.core/trim-v

しんせいたろうしんせいたろう

reg-sub する時に db から一つのキーのデータだけ取得したい場合

(rf/reg-sub
 ::name
 (fn [db _] ;; [db] だけでもいい。要はインプットデータだけが引数の時
   (:name db)))

糖衣構文 :-> が使えるので、これでOK

(rf/reg-sub
 ::name
 :-> :name)

この糖衣構文:->は、インプットデータのみが引数の時に使える。
:-> に渡すのはインプットデータを引き取る関数であれば良い。例:

(rf/reg-sub
 ::update-name
 :-> #(-> %
          :name
          (str " Updated")))

参照:
re-frame.core/reg-sub

しんせいたろうしんせいたろう

reg-sub に渡される関数について

(rf/reg-sub
   ::query-id
   (fn [input-values query-vector] ;; これ
    (....)))

この関数が取る2つの引数(input-values query-vector)のうち、query-vector はそのまま subscribe 関数に渡される

つまり、 (subscribe [:query-id arg1 arg2 ...]) のベクタ部分になる

実は、 reg-sub の呼び出し方法は3つある。
これはどのインプットデータを与えるかを特定する方法(「インプットシグナル」と呼ばれる)で分かれる。

  1. インプットシグナル無し
(rf/reg-sub
   ::query-id
   a-computation-fn)
  1. インプットシグナル関数を明示
(rf/reg-sub
   ::query-id
   signal-fn
   a-computation-fn)
  1. 糖衣構文を使う
(rf/reg-sub
   ::query-id
   :<- [:a-sub]
   :<- [:b-sub]
   a-computation-fn)
しんせいたろうしんせいたろう
  1. インプットシグナル無し
    この場合は、インプットは app-db
    a-computation-fn の第一引数に渡されるのは app-db マップになる(いつものパタン)
(rf/reg-sub
   ::query-id
   (fn [db _]
     (:name db)))
しんせいたろうしんせいたろう
  1. インプットシグナル関数を明示
    インプットに使うデータを登録して、それをサブスクライブする関数を作成。その関数を reg-sub に渡す
(rf/reg-sub
 ::sub-name
 (fn [db _]
   (:name db)))
(rf/reg-sub
 ::sub-todos
 (fn [db _]
   (:todos db)))

(rf/reg-sub
 ::name-todos-sub-explicitly
 (fn [] ;; インプットシグナル関数
   [(rf/subscribe [::sub-name])
    (rf/subscribe [::sub-todos])])
 (fn [[name todos] [_ _]]
   {:name name
    :todos (select-keys todos [2 3])}))

シグナル関数の順番通りに a-computation-fn の第一引数に渡してくれる。

しんせいたろうしんせいたろう
  1. 糖衣構文を使う。これは2の糖衣構文。
;; シグナル関数(インプット用データをサブスクライブする関数)を用意
(rf/reg-sub
 ::sub-name
 (fn [db _]
   (:name db)))
(rf/reg-sub
 ::sub-todos
 (fn [db _]
   (:todos db)))

(rf/reg-sub
 ::name-todos-sub
 :<- [::sub-name] ;; 関数の第一引数に渡る
 :<- [::sub-todos] ;; 関数の第二引数に渡る
 (fn [[name todos] [_ _]]
   {:name name
    :todos (select-keys todos [2 3])}))

インプット用にサブスクライブ登録を記述しておいて、それを :<- にベクタで渡すと順番通りに a-computation-fn の第一引数に渡してくれる。便利そう。

しんせいたろうしんせいたろう

path インターセプター

update-in のような使い方が出来るインターセプター。
たとえば、app-db の :name の値を大文字に変えたい場合、reg-event-db でこの様に記述するが、

(rf/reg-event-db
 ::event-path
 (fn [db _]
   (update db :name str/upper-case)))

(rf/path :name) を挿入すると、イベントハンドラ関数に db ではなく、キーの値をキーの名前で引き取る事が出来る。引き取った値で演算した結果を、元のキーワードの値に差し替わる。

(rf/reg-event-db
 ::event-path
 (rf/path :name)
 (fn [name]
   (str/upper-case name)))

参照:
api-re-frame.core/path

しんせいたろうしんせいたろう

enrich インターセプター

re-frame.core/#enrich

副作用だけを扱う after interceptor とは異なり、enrich は db に対して演算したりするときなどに使えます。

たとえば、TODOをカウントする時に、重複したTODOがあるかどうか確認し、あれば :duplicate? true を立てたい場合:

(def duplicate?
  (rf/enrich
   (fn [db _]
     (assoc db :duplicate? (not (distinct? (map :text (vals (:todos db)))))))))

(rf/reg-event-db
 ::todos-count
 [ duplicate?]
 (fn [db _]
   (assoc db :count (count (:todos db)))))

(※新規追加する時などに重複確認したほうがいいけど、より簡単な例にしたかったのでTODOをカウントする時にしました。)

上記のように enrich を使ってインターセプタとして定義しておいて、reg-event-db に渡せば、 after のタイミングで処理してくれるようです。
もっと具体的な例はGithubで探してみてください→ lang: clojure query: rf/enrich type: code の検索結果

しんせいたろうしんせいたろう

unwrap インターセプタ

trim-v みたいに使えるインターセプタ。

こういうディスパッチイベントの引数の時

 (dispatch [:event-id {:x 1 :y 2 :z 3}])

イベントID部分を省略できる

 (reg-event-fx
   :event-id
   [unwrap]
   (fn [{:keys [db]} {:keys [x y z]}]  ;;  [_ {:keys [x y z]}] この形の代わり
     ...)

これもGithubで具体的なコードを見たほうが早いので、リンクを貼っときます。→lang: clojure query: rf/unwrap type: code の検索結果

しんせいたろうしんせいたろう

after インターセプタ

副作用として動く関数fを定義して、
afterのタイミングで実行するためのインターセプタ

例:イベントを起こした後に、ログを出力する。

(def console-log
  #(. js/console (log "ログテスト")))

(rf/reg-event-db
 ::event-with-after
 [(rf/path :name) (rf/after console-log)]
 (fn [name]
   (str/upper-case name)))

この例は記述方法を示しただけで、全く実用的ではありませんが、
実用例的は https://github.com/search?l=Clojure&q=rf%2Fafter&type=Code
ログ出力や、local-store に書き出したり、検証したり、といった使い方のようです。
すごく使い勝手良さそうですね。

しんせいたろうしんせいたろう

reg-global-interceptor

指定したインターセプターをグローバルインターセプターとして登録。
global-interceptor は、すべてのeventチェーンに含まれます。

実例 https://github.com/search?l=Clojure&q=rf%2Freg-global-interceptor&type=Code を見てみます

例1
refactory/specs.cljs

(defn- validate-app-db
  [db _event]
  (when-not (s/valid? (s/keys) db)
    (js/console.warn (expound/expound-str (s/keys) db))))

(rf/reg-global-interceptor (rf/after validate-app-db))

おそらく全てのイベントで、 after のタイミングで検証を実行しているみたい

例2
leihs-borrow/requests.cljs

(rf/reg-global-interceptor
 (rf/->interceptor
  :id :track-mutations-ids
  :after (fn [ctx]
           (let [[ev-id :as ev] (-> ctx :coeffects :event)
                 path [:effects :db ::running-mutations-ids]]
             (case ev-id
               ::re-graph/mutate
               (update-in ctx path conj (-> ctx
                                            :effects
                                            :re-graph.internals/send-http
                                            (nth 1)))
               :re-graph.internals/http-complete
               (update-in ctx path (flip remove) #{(nth ev 2)})
               ctx)))))

rf/->interceptor の使い方ととも参考になりそうな例(内容はよくわからんけど)

参照:
re-frame/reg-global-interceptor

しんせいたろうしんせいたろう

インターセプタを作る

(->interceptor & {:as m, :keys [id before after]})

インターセプタを作成するためのユーティリティ関数(共通の処理をするための関数)
以下3つのキーワード引数を引取ます

  • :id ID
  • :before dispatch されて本イベントが起きる前に処理される関数
  • :after dispatch されて本イベントが起きた後に処理される関数
(def my-interceptor
  (->interceptor
   :id     :my-interceptor 
   :before (fn [context]
             ... `context` に更新等して返す、もしくは副作用だけ起こして更新無しで返す)
   :after  (fn [context]
             ... `context` に更新等して返す、もしくは副作用だけ起こして更新無しで返す)))

before / after どちらか一方だけでもOK。↑のreg-global-interceptor みたいな使い方をする。

re-frame.core / Writing interceptor

しんせいたろうしんせいたろう

get-coeffect

インターセプタの :before でよく使われるユーティリティ関数。 インターセプタを作る時に、context マップからデータを取得する関数
clojureのget関数のようにキーがなければ nil を返す

これも実例を見たほうが早いと思うので確認 https://github.com/search?l=Clojure&q=rf%2Fget-coeffect&type=Code

例:
weddingnext/say_num.cljs


(defn
  say-num-interceptor
  [get-answer]
  (rf/->interceptor
   :id ::say-num?
   :before ;; before インターセプタを作る時に
   (fn
     [context]
     (rf/console :log get-answer)
     (let [answer (get-answer (rf/get-coeffect context :db))] ;; app-db のデータを取得
       (rf/assoc-coeffect
        context
        ::say-num?
        (or (nil? answer)
            (not answer)
            (zero? (count answer))
            (js/isNaN answer)))))))

参照:
re-frame.core/get-coeffect

しんせいたろうしんせいたろう

assoc-coeffect

↑の例にもでてきたように
:before のインターセプタ関数を作る時に、contextの :coeffects マップのキーと値のペアを追加または更新。

参照:
re-frame.core

しんせいたろうしんせいたろう

get-effect

get-coeffect の :after 版

実例を見てみましょう: https://github.com/search?l=Clojure&q=rf%2Fget-effect&type=code

例:
mine-sweep/re_frame.cljs

(def validate-spec
  (rf/->interceptor
   :id validate-spec
   :after
   (fn [context]
     (let [new-db (rf/get-effect context :db)
           event  (-> context (rf/get-effect :event) first)]
       (when-not (spec/valid? :mine-sweep.db/db new-db)
         (throw (spec/explain-str :mine-sweep.db/db new-db)))
       context))))

参照:
re-frame.core/get-effect

しんせいたろうしんせいたろう

set-loggers!

re-frameは、警告やエラーをAPI関数consoleを介して出力する。

consoleは、デフォルトで、 log , error, warn, debug, group, groupEnd についてjs/consoleのデフォルト実装を使っている。

しかし、この関数を使えば、独自の関数でその動作を上書き出来る。

ただし、

(re-frame.core/set-loggers!  {:warn my-logger :log my-logger})

と言った形で :log :error :warn :debug :group :groupEnd のいずれかを持つマップで上書きすることが必要

例:

hop-cli/client.cljs

;; Make log level logs no-ops for production environment.
(rf/set-loggers! {:log (fn [& _])})

(defn dev-setup []
  (when goog.DEBUG
    ;; Reenable log level logs no-ops for dev environment.
    (rf/set-loggers! {:log js/console.log})
    (enable-console-print!)
    (println "Dev mode")))

参照:
re-frame.core/set-loggers!

しんせいたろうしんせいたろう

console

コンソールにログを出力出来る。 :log :error :warn :debug :group :groupEnd のいずれかのレベルを指定可

(rf/reg-event-db
 ::todos 
 (fn [db [_ arg]] 
   (rf/console :warn "arg: " arg)
   (assoc db :item arg)))

re-frame.core/console