Reactアプリは親子関係があるcomponentで構成された木構造になります。Part1では静的なcomponentを作成しましたが実際のcomponentはstateとpropsを持ちます。ClojureScriptのReagentではClojureの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の使い方を見ていきます。
- BUILDING SINGLE PAGE APPS WITH REAGENT
- Step 3: Identify the minimal (but complete) representation of UI state
- Functional programming on frontend with React & ClojureScript
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}