読者です 読者をやめる 読者になる 読者になる

Arantium Maestum

プログラミング、囲碁、読書の話題

Clojure Web Development勉強 - Reagent(その1)概要とHiccup構文

Clojure Clojurescript Web React Reagent

ReactのClojureScript WrapperであるReagentライブラリについて。

前回書いたとおり、ClojureScriptには有名なReact Wrapperが三つある。Om/Om-Next、Quiescent、そしてこのReagentである。

最初の二つの特徴を非常にざっくりと要約するなら:

  • Om/Om-Nextは精緻、強力、先進かつ複雑
  • QuiescentはClojureScriptでReactを使うため必要最低限の機能を備えた簡単かつ自由度の高いデザイン

となる。

それに対して、Reagentは

Reactの思想を汲みつつ「VectorとMapをベースにしたHiccup的DSL」と「Clojure Atomの機能を拡張したReagent Atom」の二要素を中心として極力シンプルかつClojure的に再構築したライブラリ

である。この「シンプルかつClojure的」というのがとても大事で、個人的には、習得しやすく、書き始めると自然に感じられ、書いていて気持ちが良い、と非常に好印象である。今のところClojureScriptを書く場合ほとんどReagentを使っており、少し知見もたまってきたので、他のライブラリよりも詳細に踏み込んで書いていく。

今回は第一要素である「VectorとMapをベースにしたHiccup的DSL」の話をしたい。

HTMLコードの解説

と、その前にReact/Reagentのお約束となるHTMLドキュメントのレイアウトについて一言。

大体こんな感じになるかと思う。

<html>
    <body>
        <div id="appdiv"></div>

        <script src="js/out/goog/base.js"></script>
        <script src="js/main.js"></script>
        <script>goog.require('min_reagent.core')</script>
    </body>
</html>

<script>の部分は毎度おなじみだが、その前に<div id="appdiv"></div>という新要素が追加されている。このappdivという名前が付いているDOM要素をReagentに渡してやると、Reagentがその中にどんどんDOM要素を詰め込んでページを構築してくれる。

appdivという名前は特に重要ではなく、あくまでHTMLとcljsファイルの双方で同じ名前が使われていれば良い。

<div id="appdiv"></div><script>の後に来ると、最初にロードされたreagentコードがDOM要素を見つけられずエラーを起こすので注意。必ず<div id="appdiv"></div>が先にくる必要がある。

ReagentのComponent関数

(defn my-app []
  [:div
    [:h1 "Hello Reagent"]
    [:p "ClojureScript + React + Atoms + Hiccup"]])

この時点で注意したいのは、このコンポーネント関数、あくまでVectorを返す純粋関数であって、副作用を起こすようなものはまったくない。何の変哲もないVectorなので様々なClojureのデータ構造操作関数で作ったりいじったりできる。

さらに、非常に簡単にコンポーネント化して組み合わせることができる。

(defn my-component []
  [:div
    [:h1 "Hello Reagent"]
    [:p "ClojureScript + React + Atoms + Hiccup"]])

(defn my-app []
  [:div
    [my-component]
    [my-component]])

先ほどまでmy-appという名前だった関数をmy-componentと名称変更し、my-appはそのmy-componentが二つ入っている:divにした。my-componentをmy-appから関数呼び出しするのではなく、関数自体をVectorに入れているのが注意ポイント。この例だとほとんど違いはないがより動的なコンポーネントの場合、このReagentの機能を使うための記述をする・しないで挙動や効率が大きく変わってくる。

コンポーネント関数はその名の通り関数なので、引数を取れる:

(defn my-component [name-str]
  [:div
    [:h1 "Hello " name-str]
    [:p "ClojureScript + React + Atoms + Hiccup"]])

(defn my-app []
  [:div
    [my-component "Reagent"]
    [my-component "Figwheel"]])

my-componentでname-strという引数をとり、h1内で表示する。my-appで[my-component "Reagent"]のように、関数呼び出しではなくあくまでVectorの第一要素がコンポーネント関数、第二要素(以降)が引数となる。

DOM要素のプロパティはMapで定義できる:

(defn my-component [name-str]
  [:div
    [:h1 {:on-click #(js/alert name-str)}
     "Hello " name-str]
    [:p "ClojureScript + React + Atoms + Hiccup"]])

(my-app関数は変化なし)

h1要素のon-clickプロパティに、JavaScriptのalertでname-strを出力する匿名関数を束縛した。これでh1要素をクリックするとアラートメッセージで"Reagent"や"Figwheel"と表示される。

と、ここまでの情報でHTMLのあらかたの要素とプロパティが使えてしまう。さらにfigwheelを使っていれば、いじって保存して、でグリグリとブラウザ上でページが変わっていくのでとても楽しい。ぜひお試しあれ。

ただし、コンポーネント関数自体は描画のロジックはまったく含まれていない。というか現時点では本当にただのデータ構造を返す関数である。

ReagentのRender関数

そのコンポーネント関数を受けとり、実際にブラウザ上で表示させるのを担当するのがreagentのrender関数である。

(r/render
  [my-app]
  (js/document.getElementById "appdiv"))

上記のコードでは、reagent.coreという名前空間をrに束縛してある。r/renderがreagentライブラリのrender関数である。

アプリケーショントップレベルのコンポーネント関数を含んだVectorと、document.getElementByIdというJavaScript関数を使ってアクセスした"appdiv"というidのDOM要素を引数にしている。この部分だけは純粋関数ではなくて「描画」という副作用を起こすものとなっている。

データ構造を返す純粋関数として作られているコンポーネントを組み合わせて、最終的に作り上げたトップレベコンポーネントを描画担当のrender関数に渡すのがここまでの流れ。次回はアプリケーションの状態を保持し、ステート変更をうまくComponent関数に伝達するReagent Atomの話。

今回のコード:

gist.github.com