Reactアプリは親子関係があるcomponentで構成された木構造になります。Part1では静的なcomponentを作成しましたが実際のcomponentはstateとpropsを持ちます。ClojureScriptのReagentではClojureのatomを拡張した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})) |
atomの値を参照(deref)する場合は、@stateのように@をprefixします。
(defn get-value [id] |
home component
home componentが一番親のcomponentになります。input、list、buttonのcomponentを子に持ちます
(defn home [] |
input component
text-input関数はrow関数を定義してcomponentを作成します。row関数は直接実行せずベクターで定義します。関数の実行はReagentが必要なときに自動的に行います。onChangeイベントが発火されるとset-value関数が実行されてinputフィールドの新しいの値でstateを更新します。
(defn row [label input] |
-> 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] |
list-item関数はli componentを作成します。onClkickイベントが発火されるとatomのselectionsに新しい値をセットします。
(defn list-item [id k v selections] |
->> threading macro
->> スレッディングマクロは、-> スレッディングマクロと評価の順番が異なります。->は最初に->>は最後に挿入されます。(-> 2 (+ 3) (- 7))は-2でしたが、(->> 2 (+ 3) (- 7))の場合は2になります。(+ 3 2)の結果の5が(- 7 5)のように最後に入ります。
(->> @selections |
(filter second @selections)でフィルタした結果のcollectionを(map first coll)します。
localのatom
atomのselectionsはselection-list関数内でletを使いlocalのatomとして->>マクロを使い作成されています。
(defn selection-list [id label & items] |
itemsベクターは以下のような[キーワード シンボル]のベクターを要素に持ちます。ClojureScript REPLを起動して確認してみます。
cljs.user=> (def items [[: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)) |
map関数の結果のコレクションは->>マクロで次のinto関数の引数の後ろに入りmapをつくります。
cljs.user=> (def items_map (into {} items_keys)) |
最後にatom関数の引数にmapが渡りatomを作成します。
cljs.user=> (require '[reagent.core :as reagent :refer [atom]]) |
:beerのitemがクリックされてonClickイベントが発火されると、selectionsが保持するキーワードに該当するbool値を反転させます。
cljs.user=> (swap! selections update-in [:beer] not) |
次の->>マクロを実行してクリックされた:beerキーワードの値をlocalのatomから取得します。
cljs.user=> (->> @selections (filter second) (map first)) |
globalなatomのstateはドキュメント全体のstateを保持しています。
cljs.user=> (def state (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) |