Skip to content

Instantly share code, notes, and snippets.

@knjname
Last active April 4, 2018 14:31
Show Gist options
  • Select an option

  • Save knjname/23be74a1f6a220f0226d18deeba0bbaa to your computer and use it in GitHub Desktop.

Select an option

Save knjname/23be74a1f6a220f0226d18deeba0bbaa to your computer and use it in GitHub Desktop.
(供養) re-frame 0.8.0 の変更点.md

ClojureScriptのredux風フレームワーク、re-frame 0.8.0 が出ました。

https://github.com/Day8/re-frame/blob/master/CHANGES.md#080--20160819

下記コマンドでre-frameプロジェクトテンプレートを生成できます。

# 最新用 (0.2.3)
$ lein new re-frame new-version-tryout +cider

# 古いバージョンほしい人用
$ lein new re-frame old-version-tryout --template-version 0.2.2-7 +cider

以下、何が新しくなったかを書いていきます。

同じ内容のクエリの subscribe を2回しても一度だけ評価

re-frame subscriptions are now de-duplicated. As a result, many Signal graphs will be more efficient.

こういうケースですね。

(def default-db
  {:name "re-frame"})

;; 評価された回数の計測用
(defonce  name-count (atom 0))

(re-frame/reg-sub
 :name
 (fn [db [_ where]]
   (str (:name db) " from " where "@" (swap! name-count inc))))

(defn main-panel []
  (let [name01 (re-frame/subscribe [:name "the universe"])
        name02 (re-frame/subscribe [:name "the universe"])]
    (fn []
      [:div
       [:p "Hello " @name01] ; => Hello re-frame from the universe@1
       [:p "Hello " @name02]])))  ; => Hello re-frame from the universe@1 (@2にならない)

上記で言えば、 (re-frame/subscribe [:name "the universe"]) が二回繰り返されていますが、クエリの内容が全く同じであれば、一度だけ評価されます。

たとえ、subscribeが複数コンポーネントにまたがっていてもすべてのコンポーネントで同一のクエリであれば一回の評価に集約されます。

(defn other-elem []
  (let [other-name (re-frame/subscribe [:name "the universe"])]
    (fn []
      [:p "Good bye " @other-name]))) ; => Good bye re-frame from the universe@1

0.7.0で同じことを試すと、書いた回数分評価されます。

新しいサブスクリプションハンドラ reg-sub の登場

added a new subscription handler registration function called re-frame.core/reg-sub.

端的にいうと、register-subの標準的な書き方が変わりました。

たとえば、従来 app-db が変化したら反応するようなサブスクリプションは下記のように作らなければならなかったのですが、

;; 今まで
(re-frame/register-sub
 :name
 (fn [db]
   (reaction (str "Hello " (:name @db) "!"))))
  1. db をデリファレンス (←忘れやすい。dbそのまんま来るイベントハンドラと紛らわしい。)
  2. db の中の値を加工
  3. reaction で新たな ratom として送り出す

reg-sub を使うとこんな具合に短くかけます。

;; これから
(re-frame/reg-sub
 :name
 (fn [db]
   (str "Hello " (:name db) "!")))
  1. db の中の値を加工するだけ!

他のsubscriptionを参照する場合

他のsubscriptionを参照する場合は、下記のように書けばOK。

;; もし他のsubscriptionを中で参照したい場合はこれでOK。
(re-frame/reg-sub
 :profile
 :<- [:name]
 :<- [:age]

 (fn [[name age] _] ;; 引数として渡ってくる
   {:name name :age age}))

従来型の register-sub はどこ?

まだ使えますが、消されて reg-sub-raw になりそうですね。

(defn register-sub
  [& args]
  (console :warn  "re-frame:  \"register-sub\" is deprecated. Use \"reg-sub-raw\" (look for registration of " (str (first args)) ")")
  (apply reg-sub-raw args))

register-handlerreg-event-** に変更

re-frame now supports the notion of Event Handlers accepting coeffects and returning effects. There's now three kinds of event handlers: -db, -fx and -ctx.

従来あったイベントハンドラである register-handlerreg-event-db に置き換わりました。

また似たような亜種として、 reg-event-fx, reg-event-ctx も追加されています。(詳細は後述。)

現状、ハンドラの名前空間として下記それぞれについて独立した空間が存在しています。つまり、同名のハンドラをそれぞれで登録してもバッティングしません。

  1. :event
    • (reg-event-db
    • (reg-event-fx
    • (reg-event-ctx
  2. :fx
    • (reg-fx
  3. :cofx
    • (reg-cofx
  4. :sub
    • (reg-sub

インタセプタの導入とミドルウェア終了のお知らせ

従来、イベントハンドラの実行前後に追加動作を追加するためのミドルウェアというClojureではよくある機構が用意されていましたが、0.8.0 からインタセプタという、関数よりよりデータライクな、上位の機構に置き換えられました。

(defn change-document-title-mw [handler-fn]
  (fn [db event]
    (let [new-db (handler-fn db event)
          new-title (:title new-db)] 
         (set! (.-title js/document) new-title))))

;; これが呼ばれると document.title == 'New Game!' になる。
(re-frame/register-handler
 :give-new-title
 [change-document-title-mw]
 (fn [db _]
   (assoc db :title "New Game!")))

https://github.com/Day8/re-frame/blob/develop/docs/Interceptors.md

(def change-document-title-interceptor
  {:id :change-document-title

;;; context :=
;;; {:coeffects {:event [:some-id :some-param]
;;;              :db    app-dbの元の内容}
;;;  :effects   {:db    app-dbの新しい内容
;;;              :dispatch [:an-event-id :param1]}
;;;  :queue これから実行されるインタセプタのコレクション
;;;  :stack 今まで実行されたインタセプタのコレクション}
   :after (fn [context]
            (let [new-db (get-in context [:effects :db])
                  new-title (:title new-db)]
              (set! (.-title js/document) new-title))
            context)})

(re-frame/reg-event-db
 :give-new-title
 [change-document-title-interceptor]
 (fn [db _]
   (assoc db :title "New Game!!")))

関数を返す関数(ミドルウェア)からただのマップになっています。

ミドルウェアのようにコールスタックを積まなくても(=関数の殻にどんどん包まなくとも)、インタセプタを重ねて値を加工できる仕組みになっています。

もっと複雑なインタセプタの例

effects, coeffects とは

effects とは、副作用 (side-effects) のことを指します。例としては、「Ajaxコール」とかですね。要するにアウトプットです。

coeffects (共作用) とは、effectsを生じさせるためのデータのことを指します。例としては、「『Ajaxコールしてね』というお願い」があてはまるかと思います。要するにインプットです。

だいたいこんなイメージ:

;;; 直接副作用を生じさせるのがeffects
(def effect
  (call-ajax :GET "http://www.example.com/api/users"
             :success (...)
             :failure (...)))

;;; 副作用を生じさせてほしいという指示(データ)がcoeffect
(def coeffects
  [:call-ajax {:GET "http://www.example.com/api/users"
               :success (...)
               :failure (...) }])

こんなのが副作用ですね。

  • 現在のURLの書き換え
  • document.title (タイトルバー)の書き換え
  • Cookie や LocalStorage(SessionStorage) の書き換え
  • REST APIなどの呼び出しとコールバック

従来の app-db を更新するイベントハンドラでいえば、イベントハンドラの入力値として渡される更新前の app-db が coeffects で、出力としてイベントハンドラが返す更新後の app-db が effects にあたります。

もっと有り体にいうと、それぞれ対応するキーに分類された入力値が coeffects 、 出力値が effects ですね。

coeffects :=
{
  :db ... ;; 入力値としてのapp-db (従来のイベントハンドラが受け取る値)

  :another-key-01 ... ;; ほかの入力値
}

effects :=
{
  :db ... ;; 出力値としてのapp-db (従来のイベントハンドラが出力する値)

  :another-key-01 ... ;; ほかの出力値
}

(バージョンアップ関係なし) そもそもre-frameのコールグラフとは

典型的に今までのre-frameでややこしかったのは reaction を呼ぶタイミングでした。

https://github.com/Day8/re-frame/tree/v0.7.0#a-more-efficient-signal-graph

;; 結果をreactionでくるんでいないので、そもそもダメ。
(re-frame/register-sub
  :fullname
  (fn [db]
    (str (:firstname @db) " " (:lastname @db))))

;; reactionでくるんでも、dbの更新全てに反応してしまうためダメ。(動きはする)
(re-frame/register-sub
  :fullname
  (fn [db] ;; ビクンッビクンッ
    (reaction (str (:firstname @db) " " (:lastname @db)))))

;; これなら:firstnameか、:lastnameが更新された時だけ更新されるのでOK。
;; (厳密にいうと、 (:firstname @db) や (:lastname @db) を評価した値が変化した時のみ動く。)
(re-frame/register-sub
  :fullname
  (fn [db]
    (let [firstname (reaction (:firstname @db))
          lastname (reaction (:lastname @db))]
      (reaction (str @firstname " " @lastname)))))

適切な単位でreactionを切っていく必要があります。

非効率なコールグラフを構築してしまっている場合、たとえばユーザ入力に連動させてappdbが変化するといったケースがあると、ひどい遅さにつながります。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment