re-frame document 読みながらメモ
re-frame の ドキュメント読みながらとった自分用にとったメモです。
trim-v
インターセプター
reg-event-db のハンドラ関数のイベントIDをアンダースコアで束縛するのが慣例だけど、ちょっと気持ち悪い時に使う (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)))
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")))
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つある。
これはどのインプットデータを与えるかを特定する方法(「インプットシグナル」と呼ばれる)で分かれる。
- インプットシグナル無し
(rf/reg-sub
::query-id
a-computation-fn)
- インプットシグナル関数を明示
(rf/reg-sub
::query-id
signal-fn
a-computation-fn)
- 糖衣構文を使う
(rf/reg-sub
::query-id
:<- [:a-sub]
:<- [:b-sub]
a-computation-fn)
- インプットシグナル無し
この場合は、インプットは app-db
a-computation-fn の第一引数に渡されるのは app-db マップになる(いつものパタン)
(rf/reg-sub
::query-id
(fn [db _]
(:name db)))
- インプットシグナル関数を明示
インプットに使うデータを登録して、それをサブスクライブする関数を作成。その関数を 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
の第一引数に渡してくれる。
- 糖衣構文を使う。これは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
の第一引数に渡してくれる。便利そう。
どうも、このような体質の人はほかにもいるみたいで
re-frame のドキュメントにそんな人のためのハックが書いてあったw
When used in a view function BE SURE to deref the returned value. In fact, to avoid any mistakes, some prefer to define:
(def <sub (comp deref re-frame.core/subscribe))
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)))
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 を見てみます
(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 のタイミングで検証を実行しているみたい
(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
の使い方ととも参考になりそうな例(内容はよくわからんけど)
インターセプタを作る
(->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
みたいな使い方をする。
get-coeffect
インターセプタの :before
でよく使われるユーティリティ関数。 インターセプタを作る時に、context
マップからデータを取得する関数
clojureのget
関数のようにキーがなければ nil
を返す
これも実例を見たほうが早いと思うので確認 https://github.com/search?l=Clojure&q=rf%2Fget-coeffect&type=Code
(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)))))))
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
(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))))
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
のいずれかを持つマップで上書きすることが必要
例:
;; 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")))
console
コンソールにログを出力出来る。 :log
:error
:warn
:debug
:group
:groupEnd
のいずれかのレベルを指定可
(rf/reg-event-db
::todos
(fn [db [_ arg]]
(rf/console :warn "arg: " arg)
(assoc db :item arg)))