ClojureScriptでReact 2019

こんにちは。

普段はClojureとClojureScriptだけ書いていてJavaScriptは書かないのですが、いつもより広い範囲のオーディエンスにClojureの良さを語りたくなったので参加させていただきました。

この記事ではshadow-cljsというClojureScript用のツールによって

- node&jvmさえあればClojureScriptでの開発を始められる
- 既存のnodeモジュールを簡単に利用できる
- => ClojureScriptでReactプロジェクト作るのめっちゃ楽しい!

という点を今回作った[小さなプロジェクト](https://github.com/iku000888/reagent-shadow-cljs-vis-js)を添えて主張しようと思います。

## ClojureScriptとは

狭義のClojureはRich Hickeyが作ったJVM上で動く言語です。概要は @223kazuki さんの書いた[キメるClojure高速開発](https://qiita.com/223kazuki/items/afb6341cf73a9173fda0)がオススメです。

そのヤバめな言語であるClojureをソースとして読み、JavaScriptを吐き出すコンパイラがClojureScriptです。言語の特徴以外ではGoogle Closure Compilerに依存しており、プロダクションビルドの際にその圧縮力の恩恵を受けることが期待できる点がアピールポイントです。

(ClojureとClosureで名前が衝突する点もよく話題に上がります。)


## 開発環境事情

ClojureScriptは良い物だとして、素のClojureScriptコンパイラにはいくつか課題があります。

- ClojureScriptコンパイラ自体はClojureライブラリ => Clojureを動かす環境のインストールが必要
- ホットリローディングが無い
- nodeモジュールを使うのが煩雑
    - + Google Closure Compilerによって行なわれる変数リネームによってプロダクションビルドで壊れがち

この辺りをナイスにケアしてくれる[shadow-cljs](https://shadow-cljs.github.io/docs/UsersGuide.html)というツールが数年前に登場し、nodeとJavaランタイムさえあればClojureScriptを始められる時代が来ています。

## shadow-cljsを使う

shadow-cljs自体はnpmでインストールできるcliなので使い始める敷居が低いのが特徴です。

### インストール

前提となるnodeとjvmがインストール済みの前提でそこら辺のライブラリと同じようにpackage.jsonに足すなり`npm install`するなりするだけ! 

### shadow-cljs.edn

各ビルドの設定やClojureScriptの依存ライブラリを書くファイルです。

### 開発ビルド

`npx shadow-cljs watch <build-name>` とshellに打つとコンパイルされ、開発用のwebサーバーが起動します。この状態ではソースの変更が監視されており、cljsのソースを触る度に最新のコードがブラウザにwebsocketで配信されます。最新の動作を確認するためにブラウザの更新ボタンを押す必要がない幸せを得る機能がデフォルトで付属しているのは嬉しいポイント。

### React/node moduleでデモ

今回はボタンを押すとnodeが追加、node間をドラッグするとedgeが追加される物をvis-networkとReagentというReactラッパーで作りました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124830/329b3147-545a-b616-0ed6-30696bf2e4cd.png)

コードはこちら

```clojure
(ns my.dev
  (:require
   [reagent.core :as reagent]
   ["vis-network" :as vis]))

(defn graph [{:keys [nodes edges
                     on-edge-add
                     add-edge-mode]}]
  [:div
   {:ref (fn [dom]
           (when dom
             (let [parent-dom (.-parentElement dom)
                   nw (vis/Network.
                       dom
                       #js {:nodes (vis/DataSet. (clj->js nodes))
                            :edges (vis/DataSet. (clj->js edges))}
                       (clj->js
                        {:layout {:randomSeed 111} ;;Make deterministic
                         :edges {:arrows "to"
                                 :color "red"
                                 :physics false}
                         :manipulation
                         {:addEdge (fn [data callback]
                                     (on-edge-add {:from (.-from data)
                                                   :to (.-to data)})
                                     (callback data))}}))]
               (when add-edge-mode
                 (.addEdgeMode nw))
               (doto nw
                 (.setSize
                  (.-clientWidth parent-dom)
                  (.-clientHeight parent-dom))
                 (.fit (clj->js (map :id nodes)))))))}])

(defn view []
  (reagent/with-let
    [state (reagent/atom {})]
    [:div
     [:button
      {:on-click #(swap! state
                         update
                         :nodes
                         conj
                         {:id (count (:nodes @state))
                          :label (str (count (:nodes @state)))})}
      "Add Node"]
     [graph {:nodes (:nodes @state)
             :edges (:edges @state)
             :add-edge-mode true
             :on-edge-add (fn [edge]
                            (swap! state update :edges conj edge))}]]))

(defn render-view []
  (reagent/render [view]
                  (js/document.getElementById "root")))

(defn ^:dev/after-load start []
  (js/console.log "start")
  (render-view))

(defn ^:export init []
  ;; init is called ONCE when the page loads
  ;; this is called in the index.html and must be exported
  ;; so it is available even in :advanced release builds
  (js/console.log "init")
  (render-view))

;; this is called before any code is reloaded
(defn ^:dev/before-load stop []
  (js/console.log "stop"))

```

細かい解説は需要があれば足しますが、ポイントは

- nodeモジュールが特別なことをしなくても使える
- reagentというReactラッパーが広く使われている
- ホットリローディングがデフォルト

という点です。

もしClojure/ClojureScriptに興味を持っていただいた方は、twitterでつぶやくと捕捉されやすいです。あとは[dosync radio](https://anchor.fm/dosync-radio/episodes/1--Hello--World-e4go0i/a-ai5c2l)というClojureの日本語ポッドキャストを今年から始めているので聴いてみてください。では良いお年を!