fulcroについて調べてみた

# fulcro について調べてみた

当初、「たまに使う道具としてのClojure(仮)」のタイトルとしていたのですが、どうにもまとまらなさそうなので、内容を変更させていただきました。

## om.next から fulcro へ

[om](https://github.com/omcljs/om) は、React の ClojureScript Wrapper ライブラリで、当初はそれなりに勢いがあったように思いますが、途中から実装方針が変更となってom.next と呼ばれるようになってからは、どういうわけか開発ペースが落ちてしまったようで、今では開発がほとんどストップしているような状況です。

自分は、om.next の面白さ、可能性を信じてウォッチしていたのですが、なかなか動きがありませんでした（1.0.0-alpha3 が 2015/10/16 に出て、2018/06/02 に 1.0.0-beta4 がやっと出た位の勢いです...）。

そんな om.next ですが、一方でひょんなことから [fulcro](https://github.com/fulcrologic/fulcro) ([WebSite](http://fulcro.fulcrologic.com/))というものを知りました。fulcro は当初 om.next の add on library からスタートしたようで、om.next の血(?)を受け継ぎながら改良が進み現在も活発に開発が進んでいるようです。

そこで、fulcro について実際にコードを書きつつ少し調べてみたので、（自分用メモの意味も含め）記しておきたいと思います。

## Getting started

それでは早速触ってみます。

### 前提条件

* leiningen (>= 2.8.1)
* node、npm (> ? 最近のやつならたぶん大丈夫)

### サンプルプロジェクト作成

leiningen のテンプレートがあるので、サンプルプロジェクトを作ってみます。

```shell
lein new fulcro fulcro-sample-app
```

fulcro では ClojureScript のコンパイルに shadow-cljs を使用しているので、`npm install` も忘れずに。

```shell
cd fulcro-sample-app
npm install
```

### 開発環境の起動

開発環境を起動するには、

* ClojureScript のコンパイル環境(shadow-cljs)の起動
* Backend となる http server の起動

を行う必要があります。

```shell
npx shadow-cljs server
# or
# node ./node_modules/.bin/shadow-cljs server
```

に続いて、別端末から

```shell
lein repl
user=> (start)
```

を実行、でOKです。この状態で、http://localhost:9630 を表示させると、

<img width="341" alt="スクリーンショット 2018-12-16 19.00.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/3208/ab4c184c-0e88-f3fb-9ae2-92c474c0347c.png">

の状態となり、

<img width="421" alt="スクリーンショット 2018-12-16 19.00.32.png" src="https://qiita-image-store.s3.amazonaws.com/0/3208/c364fad2-c19a-9099-b087-947d671cd680.png">

Builds の中から main を選んで

<img width="314" alt="スクリーンショット 2018-12-16 19.00.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/3208/b589d4ef-6be4-73af-01a7-98929abbd31f.png">

[start watch] をクリックすると、cljs をビルド(watch)してくれます。
ここまでくれば、http://localhost:3000 を開くと、サンプルの画面が表示されます。

(下図は、後述するカウンターコンポーネントを追加した後の状態です)

<img width="428" alt="スクリーンショット 2018-12-16 19.01.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/3208/3c2366c5-2912-cf7a-2eda-1e13d4f8126c.png">

### サンプルコンポーネント解説

基本的なクライアントの構造を理解するため、説明対象を絞って説明していきます。

```
src
├── main
│  └── fulcro_sample_app
│     ├── client.cljs          ...(1)
│     └── ui
│        ├── components.cljs   ...(3)
│        └── root.cljs         ...(2)

```

#### (1) client.cljs

クライアント側のエントリーポイントになります。ここから次の(2)に定義された Root コンポーネントをマウントしています。

#### (2) root.cljs

Root コンポーネントを`defsc`マクロで定義しています。

```clojure
(ns fulcro-sample-app.ui.root
  (:require
    [fulcro.client.dom :as dom :refer [div]]
    [fulcro.client.primitives :as prim :refer [defsc]]
    [fulcro-sample-app.ui.components :as comp]))

(defsc Root [this {:keys [root/message]}]
  {:query         [:root/message]
   :initial-state {:root/message "Hello!"}}
  (div :.ui.segments
    (div :.ui.top.attached.segment
      (div :.content
        "Welcome to Fulcro!"))
    (div :.ui.attached.segment
      (div :.content
        (comp/ui-placeholder {:w 50 :h 50})  ;; *1
        (div message)
        (div "Some content here would be nice.")))))
```

このコードでは、次のことを定義しています。

* コンポーネントの名前：`Root`
* コンポーネントの props( `[this {:keys [root/messages]}]` の `root/messages` が propsとなる。)
* このコンポーネントの状態(state)を取り出すための`query`と、初期状態`initial-state` (省略可能)
* このコンポーネントの(Reactでいう) render で返すもの：`(div ...)`の内容

Rootコンポーネントから (3) components.cljs に定義されている `ui-placeholder` が呼び出されています（*1の部分）。

#### (3) components.cljs

```clojure
(ns fulcro-sample-app.ui.components
  (:require
    [fulcro.client.primitives :as prim :refer [defsc]]
    [fulcro.client.dom :as dom]))

(defsc PlaceholderImage
  [this {:keys [w h label]}]
  (let [label (or label (str w "x" h))]
    (dom/svg #js {:width w :height h}
      (dom/rect #js {:width w :height h :style #js {:fill        "rgb(200,200,200)"
                                                    :strokeWidth 2
                                                    :stroke      "black"}})
      (dom/text #js {:textAnchor "middle" :x (/ w 2) :y (/ h 2)} label))))

(def ui-placeholder (prim/factory PlaceholderImage))
```

(2) の Root コンポーネントの render 内では `ui-placeholder` が参照されていますが、これは `defsc` で定義されたものを、`prim/factory` で作成したインスタンス、となります。

(2) の Root コンポーネントでは、状態を持っていたため、`query`とか`initial-state` を定義していましたが、(3) PlaceholderImage は状態を持たないので、`query`、`initial-state` は定義していません。

ここまで、leiningen のテンプレートで作成されたコンポーネントについて説明してきました。

### om.next との違い

これまでのところでの om.next との違いについて軽く触れておきます。

#### 状態の取り扱い

om.next だと、状態に対する読み取り(read)および更新(mutation)を、`Reconciler`管理下の parser としてコーディングしていましたが、`defsc`マクロでうまく隠蔽されるようになっています（mutation については後述）。

#### React の lifecycle method

om.next だと、`defui` の中で `render` を記述していましたが、`defsc`マクロにおいては単純に dom tree を宣言的に記述しておくだけ、となりました。

同様に、状態の初期化についても、従来だと `componentDidmount`等で初期状態を与える、といったことをしていたかと思いますが、`defsc`の `initial-state` に記述しておくだけでよくなりました。

どちらも、より宣言的になりコードがすっきり書けるようになったといえるかと思います(ただし、従来どおり lifecycle method を呼びたいというニーズに答えるために、`defsc`マクロでも記述できるようになっているようです)。

## 応用編 Counter Component

ここからは応用編その１として、ボタンを押したらカウントアップするだけのコンポーネントを作ってみます。カウントした値はデータベースに保存します。

この例では、

* 状態全体の構造の考え方と query の記述
* mutation
* remoting

について示したいと思います。

### 関連ソース

```
src
├── main
│  ├── config
│  │  ├── defaults.edn         ...(1)
│  └── fulcro_sample_app
│     ├── client.cljs           ...(6)
│     ├── server_components
│     │  ├── database.clj      ...(2)
│     │  └── middleware.clj    ...(3)
│     └── ui
│        ├── counter.clj        ...(7)
│        ├── counter.cljs       ...(4)
│        └── root.cljs          ...(5)
```

#### DB関連

fulcro の template で生成されたソースでは、コンポーネントの状態管理に [mount](https://github.com/tolitius/mount)  を使用するようになっていますので、データベース関連のコードを以下のように追加していきます。

* defaults.edn (1)

データベース接続情報を追記(好みに合わせて適当に)。

```clojure
{
 ;; 以下を追加
 ;; datasource configuration option
 :database {;; : snip
            :adapter            "postgresql" ;; or other database
            :username           "db-user-name"
            :password           "db-password"
            :database-name      "db-database-name"
            :server-name        "db-server-hostname"
            :port-number        5432
            ;; : snip
            }}
```

* database.clj (2)

データソース取得を `hikari-cp` で行うようにしました。`mount` を使って状態管理するので、`mount.core/defstate` で宣言しておきます。

```clojure
(ns fulcro-sample-app.server-components.database
  (:require
   [mount.core :refer [defstate]]
   [hikari-cp.core :refer [make-datasource]]
   [fulcro-sample-app.server-components.config :refer [config]]))

(defstate database
  :start (make-datasource (:database config))
  :stop (.close database))
```

* middleware.clj (3)

作成したデータソースは、`fulcro-sample-app.server-components.database/database` という var で参照できるので、middleware 経由で参照できるようにしておきます。

```clojure
(ns fulcro-sample-app.server-components.middleware
  (:require
    ;; 中略
    [fulcro-sample-app.server-components.database :refer [database]]
    [mount.core :refer [defstate]]
    [fulcro.server :as server]
    ;; 中略
    ))

;; 中略
;; ================================================================================
;; Replace this with a pathom Parser once you get past the beginner stage.
;; This one supports the defquery-root, defquery-entity, and defmutation as
;; defined in the book, but you'll have a much better time parsing queries with
;; Pathom.
;; ================================================================================
(def server-parser (server/fulcro-parser))

(defn wrap-api [handler uri]
  (fn [request]
    (if (= uri (:uri request))
      (server/handle-api-request
        ;; Sub out a pathom parser here if you want to use pathom.
        server-parser
        ;; this map is `env`. Put other defstate things in this map and they'll be
        ;; in the mutations/query env on server.
        {:config config
         :datasource database        ;; ★add
         }
        (:transit-params request))
      (handler request))))
```

server side の実装は、上記コードの `server-parser` を経由して、 `defquery-root` / `defquery-entity` / `defmutation` で宣言した関数を呼び出す形となります。

具体的な実装例は (7) で示します。

#### client側の実装

* counter.cljs (4)

Counter コンポーネントを追加します。

```clojure
(ns fulcro-sample-app.ui.counter
  (:require
   [fulcro.client.primitives :as prim :refer [defsc]]
   [fulcro.client.mutations :refer [defmutation]]
   [fulcro.client.dom :as dom]
   [sablono.core :refer [html]]))

(defmutation bump-number [ignored] ;; ④
  (action [{:keys [state] :as param}] ;; ⑤
    (swap! state update-in [:root/counter :counter/cnt] inc))
  (remote [env] true)) ;; ⑥

(defsc Counter
  "simple counter example component"
  [this {:keys [counter/cnt]}]    ;; ①
  {:query         [:counter/cnt]  ;; ②
   :initial-state {:counter/cnt 1}}
  (html
   [:button {:on-click #(prim/transact! this `[(bump-number {})])} ;; ③
    "You've clicked this button [" cnt "] times."]))

(def ui-counter (prim/factory Counter))
```

このコンポーネントでは、以下の点について記述しています。

- ① 現在のカウント状態は `:counter/cnt` キーに格納
- ② それを取り出す query は `:counter/cnt` で表現される
- ③ ボタンクリック時に `prim/transact` が呼び出され、そのパラメータで指定された query が実行される。ここでは `[(bump-number {})]` となっているので、`bump-number` という名の mutation (引数なし) が実行される
- ④ mutation は `defmutation` マクロで宣言(引数なしなのでたまたま`[ignored]`となっているが、実際には`[{:keys [param1 param2]}]` のような形となる)。
- ⑤ mutation による処理本体は、`action` の中に記述。このコンポーネントでは、状態は `:counter/cnt` に格納されているが、root からは `:root/counter` にぶらさげる予定なので、root からみたカウントの値は `{:root/counter {:counter/cnt 値}}`の形となり、`update-in` で更新(ここではinc) する形となる。
- ⑥ `(remote [env] true)` を記述しておくと、この mutation が実行された際に remote の mutation を実行するようになる。remote の mutation 実装については (7)に記載。

* root.cljs (5)

Root コンポーネントから Counter コンポーネントを参照するようにします。

```clojure
(ns fulcro-sample-app.ui.root
  (:require
    [fulcro.client.dom :as dom :refer [div]]
    [fulcro.client.primitives :as prim :refer [defsc]]
    [fulcro-sample-app.ui.components :as comp]
    [fulcro-sample-app.ui.counter :as counter]  ;; ★追加
    [taoensso.timbre :as log]
    [sablono.core :refer [html]]))

(defsc Root [this {:keys [root/message
                          root/counter]}]  ;; ★追加
  {:query         [:ui/react-key :root/message
                   {:root/counter (prim/get-query counter/Counter)}] ;; ★追加
   :initial-state (fn [params]
                    {:root/message "Hello!"
                     :root/counter (prim/get-initial-state counter/Counter {})})} ;; ★追加
  (html
   [:div.ui.segments
    ;; 中略
    ;; counter example ★追加
    [:div.ui.attached.segment
     [:div.content
      "Counter example" [:span " "]
      (counter/ui-counter counter)]]]))
```

ここでは、query と initial-state について補足しておきます。

`query` について：`:root/counter` というキーに、Counter コンポーネントの状態をまるっとぶら下げる、という時に `{:root/counter ...}` のように map で表します(これをquery の **join** と呼ぶ)。また、コンポーネントがどんな query を持っているかは、`prim/get-query` を使って取得できます。

`initial-state` について：initial-state には、map による固定値の他に、map を返却する関数を指定できます。コンポーネント自身の状態あるいはぶら下がるコンポーネントの状態(のtree)は、initial-state で初期化しておくべきです(初期化していない場合におかしな動きをするケースがあり、少しはまりました)。

* client.cljs (6)

Counter コンポーネントの query は、`:counter/cnt` でした。ただ今回はサーバ上の値を読みに行かないといけないので、その設定をしてあげる必要があります。そのための設定が、下記 `:started-callback` の指定となります。

```clojure
(ns fulcro-sample-app.client
  (:require [fulcro.client :as fc]
            [fulcro-sample-app.ui.root :as root]
            [fulcro-sample-app.ui.counter :as counter] ;; ★ add
            [fulcro.client.network :as net]
            [fulcro.client.data-fetch :as df]))
;; 中略
(defn ^:export init []
  (reset! app (fc/new-fulcro-client
                ;; This ensures your client can talk to a CSRF-protected server.
                ;; See middleware.clj to see how the token is embedded into the HTML
                :networking {:remote (net/fulcro-http-remote
                                       {:url                "/api"
                                        :request-middleware secured-request-middleware})}
                :started-callback (fn [app]                     ;; ★add
                                    (df/load app :root/counter counter/Counter))))
  (start))
```

ここまでのコードで、client 側の実装は完了しました。次からサーバ側の実装となります。

#### backend側の実装

* counter.clj (7)

Counter コンポーネントのサーバ側実装について説明します。

```clojure
(ns fulcro-sample-app.ui.counter
  (:require
   [fulcro.server :refer [defmutation defquery-root defquery-entity]]
   [clojure.java.jdbc :as j]))

(defn- get-curval
  [conn]
  ;; 中略： DB上のカウント状態を保持したテーブルの値を読み出し返す
)

(defquery-root :root/counter ;; ①
  (value [{:keys [parser query datasource] :as env} params] ;; ②
    (j/with-db-connection [conn {:datasource datasource}]
      {:counter/cnt (get-curval conn)}))) ;; ③

(defmutation bump-number [ignored] ;; ④
  (action [{:keys [datasource] :as env}] ;; ⑤
    (j/with-db-transaction [t-conn {:datasource datasource} {:read-only? false}]
      ;; 中略：カウント状態を 1増やす
      {:counter/cnt (get-curval t-conn)}))) ⑥
```

* ① `defquery-root` で query 時に呼び出される関数を定義します。`:root/counter` は、query のキーとなります。
* ② `value` の中に実際の処理を記述します。
* ③ここで注意すべきは、戻り値の型です。クライアント側の Counter コンポーネントの状態は、root から見たときに、`{:root/counter {:counter/cnt 値}}` という形でした。この `value` が返すべきものは、`{:root/counter ...}` の `...` の部分となるので、`{:counter/cnt 値}` を返す必要があります（説明が下手ですが）。
* ④サーバ側の mutation を定義します。ここでは `bump-number` という symbol が来たときの mutation 定義となります。
* ⑤`action` の中に実際の処理を記述します。「middleware.clj (3)」で、`:datasource` にデータソースを埋め込んでいるので、env から取得できます。
* ⑥ カウントアップした後に戻り値を返します。これも同様に`{:counter/cnt 更新後の値}` を返します。

### om.next との違い(remoting)

om.next では、query / mutation ともに「特定の様式」での multi method を定義し、reconciler にサーバ送受信コードを記述してやることで、remoting は実行できました。

以下は、om.next での client side コードの例を示します(今回の例とは異なります。また一部抜粋です)。

```clojure
(defmethod read :root/text
  [{:keys [state query ast] :as env} k params]
  (if-let [v (get @state k)]
    {:value (om/db->tree query v @state) :remote true}
    {:value "not-found"}))

(defmethod mutate 'root/update-text
  [{:keys [state] :as env} _ {:keys [text]}]
  {:value {:keys [:root/textinput]}
   :action (fn [] (swap! state assoc-in [:root/textinput :text] text))})

(def parser (om/parser {:read read :mutate mutate}))

(def reconciler
  (om/reconciler
   {:state app-state
    :normalize true
    :parser parser
    :send (util/transit-post "/api")}))

(defn transit-post [url]
  (fn [edn callback-fn]
    (.send XhrIo url
      (fn [e]
        (this-as this
           (
               ;; 中略
           )))
      "POST" (t/write (t/writer :json) edn)
      #js {"Content-Type" "application/transit+json"})))
```

om.next では read / mutate のマルチメソッド内でボイラープレートコードをたくさん書く必要があったのが、fulcro ではそれがほとんどなくなり、ずいぶんスッキリしたように思います。

## 所感

om.next では、状態の管理(read / mutate) が UI と完全に分離していて、慣れればソースがきれいに書けたように思いますが、ボイラープレートコードがじゃまをしてせっかくのコンセプトもいまひとつ生かされていない印象でした。fulcro はその点うまく整理できているように思います。

あと、fulcro を使いこなす上で避けて通れないのが query 関連だと（個人的には）思っています。query を除けばほぼ Clojure/Sciprt、React、の知識だけでだいたいなんとかなる、のですが...。query に関しては [Developer Guide](http://book.fulcrologic.com/#_eql_the_query_and_mutation_language) を読んで理解していくしかなさそうです。

GraphQL といい、こういうのって流行りなのでしょうか。関連しそうなものとして、

* [EQL](https://github.com/edn-query-language/eql) - EDN query language
* [Pathom](https://wilkerlucio.github.io/pathom/) - A Clojure library designed to help you write Clojure(script) graph query processing parsers for the query notation used by [EQL](https://github.com/edn-query-language/eql) 
* [Walkable](https://walkable.gitlab.io/)  - Clojure(script) SQL library for building APIs

とかあるのですが、使える（自分のモノになる）かどうかは別として、面白そうなのは間違いないですね。今後ぼちぼち追っかけてみたいと思います。

今回検証したコード（プラスアルファ）は、[Github](https://github.com/ponkore/fulcro-sample-app) にあげておきました。

以上です。