0%

Reagent入門 - Part2: atomとcomponentとthreading macro

Reactアプリは親子関係があるcomponentで構成された木構造になります。Part1では静的なcomponentを作成しましたが実際のcomponentはstateとpropsを持ちます。ClojureScriptReagentではClojureatomを拡張したatomを使いReactのstateを抽象化します。stateとpropsの厳密な区別が不要で、Reactよりも複雑なモデルをcomponentで作成することができます。

Reactのstateとprops

Reactアプリはcomopnentで構成します。子componentの共通の親componentがstateを持ち、子componentへpropsとして渡します。stateを持つだけの親componentを作る場合もあります。親が管理をして子は使う関係になります。

stateの特徴

  • 値が変化する
  • stateが変更されるとcomponentは再描画される

propsの特徴

  • immutableで値が変化しない
  • 親から渡される値
  • stateや他のpropsから計算される値

atom

Reagentではstateをatomで抽象化します。

  • steteを定義して値の変化を監視する
  • イベントハンドラが変化を受け付けて値を更新する

Reagent独自のatom

Reagentのatom(ratom)は通常のClojureのatomと同じように動作します。atomの値が変化があると、derefしているすべてのcomponentが自動的に再描画される点が通常のatomとは異なります。

atomの操作

副作用の関数を使ってatomの値を更新します。副作用の関数はreset!swap!のように!でsuffixされています。

使い方

以下のサイトを参考にしてatomとcomponentの使い方を見ていきます。

globalなatom

次の例ではdocument全体のstateを管理するglobalなatomを定義しています。

(def state (atom {:doc {} :saved? false}))

(defn set-value! [id value]
(swap! state assoc :saved? false)
(swap! state assoc-in [:doc id] value))

atomの値を参照(deref)する場合は、@stateのように@をprefixします。

(defn get-value [id]
(get-in @state [:doc id]))

home component

home componentが一番親のcomponentになります。input、list、buttonのcomponentを子に持ちます

(defn home []
[:div
[:div.page-header [:h1 "Reagent Form"]]

[text-input :first-name "First name"]
[text-input :last-name "Last name"]
[selection-list :favorite-drinks "Favorite drinks"
[:coffee "Coffee"]
[:beer "Beer"]
[:crab-juice "Crab juice"]]

(if (:saved? @state)
[:p "Saved"]
[:button {:type "submit"
:class "btn btn-default"
:onClick save-doc}
"Submit"])])

input component

text-input関数はrow関数を定義してcomponentを作成します。row関数は直接実行せずベクターで定義します。関数の実行はReagentが必要なときに自動的に行います。onChangeイベントが発火されるとset-value関数が実行されてinputフィールドの新しいの値でstateを更新します。

(defn row [label input]
[:div.row
[:div.col-md-2 [:label label]]
[:div.col-md-5 input]])

(defn text-input [id label]
[row label
[:input
{:type "text"
:class "form-control"
:value (get-value id)
:on-change #(set-value! id (-> % .-target .-value))}]])

-> threading macro

-> スレッディングマクロは左から右に連続して次の関数の関数を実行します。What does -> do in clojure?に例があります。(+ 2 3)の結果の5が次の関数の先頭に送信されます。(- 5 7)を評価するので結果は-2になります。

(-> 2 (+ 3) (- 7))

list component

comopnentの中でlocalなatomをletで作成することもできます。

(defn selection-list [id label & items]
(let [selections (->> items (map (fn [[k]] [k false])) (into {}) atom)]
(fn []
[:div.row
[:div.col-md-2 [:span label]]
[:div.col-md-5
[:div.row
(for [[k v] items]
[list-item id k v selections])]]])))

list-item関数はli componentを作成します。onClkickイベントが発火されるとatomのselectionsに新しい値をセットします。

(defn list-item [id k v selections]
(letfn [(handle-click! []
(swap! selections update-in [k] not)
(set-value! id (->> @selections
(filter second)
(map first))))]
[:li {:class (str "list-group-item"
(if (k @selections) " active"))
:on-click handle-click!}
v]))

->> threading macro

->> スレッディングマクロは、-> スレッディングマクロと評価の順番が異なります。->は最初に->>は最後に挿入されます。(-> 2 (+ 3) (- 7))-2でしたが、(->> 2 (+ 3) (- 7))の場合は2になります。(+ 3 2)の結果の5(- 7 5)のように最後に入ります。

(->> @selections
(filter second)
(map first))

(filter second @selections)でフィルタした結果のcollectionを(map first coll)します。

localのatom

atomのselectionsはselection-list関数内でletを使いlocalのatomとして->>マクロを使い作成されています。

(defn selection-list [id label & items]
(let [selections (->> items (map (fn [[k]] [k false])) (into {}) atom)]
...

itemsベクターは以下のような[キーワード シンボル]のベクターを要素に持ちます。ClojureScript REPLを起動して確認してみます。

cljs.user=> (def items [[:coffee "Coffee"] [:beer "Beer"] [:crab-juice "Crab juice"]])
[[:coffee "Coffee"] [:beer "Beer"] [:crab-juice "Crab juice"]]

->>マクロでitemsはmap関数の後ろの引数に入ります。[[k]]でベクターをdestructuringして先頭のキーワードをkのシンボルにバインドします。map関数ではitemの要素ごとに[キーワード false]の新しいベクターを返します。

cljs.user=> (def items_keys (map (fn [[k]] [k false]) items))
([:coffee false] [:beer false] [:crab-juice false])

map関数の結果のコレクションは->>マクロで次のinto関数の引数の後ろに入りmapをつくります。

cljs.user=> (def items_map (into {} items_keys))
{:coffee false, :beer false, :crab-juice false}

最後にatom関数の引数にmapが渡りatomを作成します。

cljs.user=> (require '[reagent.core :as reagent :refer [atom]])
nil
cljs.user=> (def selections (atom items_map))
#<Atom: {:coffee false, :beer false, :crab-juice false}>
cljs.user=> @selections
{:coffee false, :beer false, :crab-juice false}

:beerのitemがクリックされてonClickイベントが発火されると、selectionsが保持するキーワードに該当するbool値を反転させます。

cljs.user=> (swap! selections update-in [:beer] not)
{:coffee false, :beer true, :crab-juice false}

次の->>マクロを実行してクリックされた:beerキーワードの値をlocalのatomから取得します。

cljs.user=> (->> @selections (filter second) (map first))
(:beer)

globalなatomのstateはドキュメント全体のstateを保持しています。

cljs.user=> (def state (atom {:doc {} :saved? false}))
#<Atom: {:doc {}, :saved? false}>

list componentの中で保持しているlocalなatomをクリックイベントによって更新したあと、globalなatomのstateを更新します。選択されたitem componentの:beerキーワードとlist componentの:favorite-drinksキーワードを使いglobalのatomを更新します。

cljs.user=> (swap! state assoc :saved? false)
{:doc {}, :saved? false}
cljs.user=> (swap! state assoc-in [:doc :favorite-drinks] :beer))
{:doc {:favorite-drinks :beer}, :saved? false}