ClojureScript & ReagentでReact入門してみた

そろそろReactに入門したいなぁ、でもClojurianとしてはJavaScriptやTypeScriptではなく[ClojureScript](https://clojurescript.org/)で書きたいなぁ、ということでClojureScriptと[Reagent](https://reagent-project.github.io/)で[React公式チュートリアル](https://reactjs.org/tutorial/tutorial.html)に取り組んでみた。

最終的なReactチュートリアルアプリ(tic-tac-toe; 三目並べ)の実装例はこちら:
[lagenorhynque/react-tutorial](https://github.com/lagenorhynque/react-tutorial)

# React->Reagentのポイント

ReactのコードをReagentで書く際のポイントを簡単にまとめてみると、

| React                 | Reagent                                                |
| :---                  | :---                                                   |
| `JSX` (React element) | [Hiccup](https://github.com/weavejester/hiccup)風のDSL |
| React component       | ClojureScriptの関数                                    |
| `props`               | コンポーネント関数の引数                               |
| `state`               | `reagent.core/atom`                                    |

# ClojureScript/Reagentの始め方

## プロジェクト生成

簡単に始めるために、ここでは[shadow-cljs](https://github.com/thheller/shadow-cljs)のテンプレート[create-cljs-app](https://github.com/filipesilva/create-cljs-app)でClojureScript/Reagentプロジェクトを生成する(※ Node.jsとJava (JDK)は事前にインストールしておく)。

```sh
$ npx create-cljs-app react-tutorial
```

生成されるプロジェクトの構成は以下の通り。
ここでは、直接 `src/app/core.cljs` にチュートリアルアプリを実装してみる。

```sh
$ tree -a -I '.git|node_modules' react-tutorial/
react-tutorial/
├── .clj-kondo
│   └── config.edn
├── .gitignore
├── .zprintrc
├── README.md
├── package-lock.json
├── package.json
├── public
│   ├── css
│   │   └── style.css
│   ├── favicon.ico
│   └── index.html
├── shadow-cljs.edn
└── src
    ├── app
    │   ├── cards.cljs
    │   ├── core.cljs
    │   ├── hello.cljs
    │   └── hello_cards.cljs
    └── e2e
        └── core.cljs

6 directories, 15 files
```

## テスト起動

ちなみに、この時点でアプリケーションを起動してみると、 `http://localhost:3000/` から自動生成された初期画面が確認できる。

```sh
$ cd react-tutorial/
$ npm start
```

**実行イメージ(with [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi))**

![実行イメージ(初期)](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33430/b8fa5d89-e8c2-aaf1-5f21-4d78f8f24316.png)

## チュートリアルの事前準備

最後に、チュートリアルの[Starter Code](https://codepen.io/gaearon/pen/oWWQNa)を参考に、土台となるHTMLの差分を[public/index.html](https://github.com/lagenorhynque/react-tutorial/blob/master/public/index.html)に、CSSを[public/css/style.css](https://github.com/lagenorhynque/react-tutorial/blob/master/public/css/style.css)に反映すれば準備完了。

# Reactコンポーネントを作る/使う

[Reagent公式ページ](https://reagent-project.github.io/)や[doc](https://github.com/reagent-project/reagent/tree/master/doc)を参考に、まずはReagentでReactコンポーネントを作ってみる。

## コンポーネントの定義

例えば、[Starter Code](https://codepen.io/gaearon/pen/oWWQNa?editors=0010)にあるReactコンポーネント `Game` は、JavaScriptで以下のように書かれている。

```js
class Game extends React.Component {
  render() {
    return (
      <div className="game">
        <div className="game-board">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}
```

Reagentでは、JSXの代わりに[Hiccup](https://github.com/weavejester/hiccup)スタイルのDSL(ClojureScriptのVector, Map, Keyword, etc.)でHTMLを表現できる。

そして、ReactコンポーネントはHiccup風の式を返すClojureScriptの関数として表現できる。

したがって、上の `Game` コンポーネントはReagentで次のような `game` 関数として定義できる。

※ Clojure/Lispでは関数や変数の命名にUpperCamelCaseやlowerCamelCaseではなくlisp-case(別名kebab-case)を使うので、Lisp風の命名に変えてある。

```clj
(defn game []
  [:div.game
   [:div.game-board
    [board]]
   [:div.game-info
    [:div
     ;; status
     ]
    [:ol
     ;; TODO
     ]]])
```

## コンポーネントの利用

上記の例にすでに表れているように、既存のコンポーネントを利用するにはReactのJSXで

```js
<Board />
```

のように書くところを、Reagentでは

```clj
[board]
```

のように関数名を `[]` で括って書く。

※ `board` コンポーネントもReagentでは関数の形で定義するので `()` で括って関数として呼び出すこともできるが、 `[]` で括ることでReactコンポーネントとして扱われ効率的に再描画される対象になる。

# コンポーネントの `props` を扱う

## `props` の導入

Reactコンポーネントに設定情報を渡すために利用する `props` は、Reagentではコンポーネント関数の引数として表現する。

例えば、[Passing Data Through Props](https://reactjs.org/tutorial/tutorial.html#passing-data-through-props)の `Square` コンポーネントは `props` を利用して以下のように書かれている。

```js
class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {this.props.value}
      </button>
    );
  }
}
```

これをReagentで書くと、

```clj
(defn square [& {:keys [value]}]
  [:button.square
   value])
```

あくまでClojureScriptの関数引数なので、このように可変長引数や分配束縛によるオプション/デフォルト引数なども利用することができる(もちろん通常の位置引数でも良い)。

## function components

Reactでは `render` メソッドのみのReactコンポーネントを `function components` として定義できる。

ここまでのReagentでのコンポーネント定義方法([A Simple Function](https://github.com/reagent-project/reagent/blob/master/doc/CreatingReagentComponents.md#form-1-a-simple-function))はちょうどそれに相当するものだといえる。

※ Reactのclassによるコンポーネント定義に相当する定義方法はこちら: [A Class With Life Cycle Methods](https://github.com/reagent-project/reagent/blob/master/doc/CreatingReagentComponents.md#form-3-a-class-with-life-cycle-methods)

[Function Components](https://reactjs.org/tutorial/tutorial.html#function-components)にある `Square` コンポーネントの最終形も以下のように実装できる。

```js
function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
      {props.value}
    </button>
  );
}
```

↓↓↓

```clj
(defn square [& {:keys [value on-click]}]
  [:button.square {:on-click on-click}
   value])
```

ちなみに、このように `props` を持つコンポーネントを利用するには、Reactで

```js
<Square
  value={this.props.squares[i]}
  onClick={() => this.props.onClick(i)}
/>
```

と書くのに対して、Reagentでは

```clj
[square
 :value (squares i)
 :on-click #(on-click i)]
```

のように書ける。

# コンポーネントの `state` を扱う

## `state` の導入

最後に、Reactコンポーネントに個別の状態 `state` を持たせてみる。

[Making an Interactive Component](https://reactjs.org/tutorial/tutorial.html#making-an-interactive-component)では、 `Square` コンポーネントが内部状態として `state.value` を持ち、buttonのonClickで `null` から `'X'` に更新する。

```js
class Square extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: null,
    };
  }

  render() {
    return (
      <button
        className="square"
        onClick={() => this.setState({value: 'X'})}
      >
        {this.state.value}
      </button>
    );
  }
}
```

このような更新可能な状態(つまり `state`)を扱うために、Reagentでは [`reagent.core/atom`](http://reagent-project.github.io/docs/master/reagent.core.html#var-atom) を利用する。

これは、コンポーネントの再描画に関わっていることを除いて [`clojure.core/atom`](https://clojure.github.io/clojure/clojure.core-api.html#clojure.core/atom) と同様なので、 `@` で [`deref`](https://clojure.github.io/clojure/clojure.core-api.html#clojure.core/deref) したり [`reset!`](https://clojure.github.io/clojure/clojure.core-api.html#clojure.core/reset!) したり [`swap!`](https://clojure.github.io/clojure/clojure.core-api.html#clojure.core/swap!) したりできる(`atom` を `deref` しているコンポーネントは自動的に再描画される)。

`atom` による状態を `def` でnamespaceのトップレベルに定義することもできるが、ここではコンポーネントの個々のインスタンスにローカルな内部状態として持たせたいので、 `atom` を参照するクロージャを返す方法([A Function Returning A Function](https://github.com/reagent-project/reagent/blob/master/doc/CreatingReagentComponents.md#form-2--a-function-returning-a-function))を利用する。

つまり、以下のようにReact要素(Hiccup風の式)をラムダ式でラップし、外側の `atom` を参照させる。

```clj
(ns app.core
  "This namespace contains your application and is the entrypoint for 'yarn start'."
  (:require
   [reagent.core :as reagent]))

(defn square []
  (let [value (reagent/atom nil)]
    (fn []
      [:button.square {:on-click #(reset! value "X")}
       @value])))
```

これにより、 `square` コンポーネントはインスタンスごとに変更可能なstateとして `value` を持ち、現在の値を表示するとともに、onClickで値を更新することが可能になる。

# チュートリアルの実装例

以上のことを踏まえて、適宜[Reagent公式ページ](https://reagent-project.github.io/)も参照しつつReactチュートリアルを進めてみると、ClojureScriptとReagentでシンプルにチュートリアルアプリが実装できた。
[lagenorhynque/react-tutorial](https://github.com/lagenorhynque/react-tutorial)

チュートリアル本編を最後まで進めたClojureScriptコードの最終形は以下の通り。

cf. 公式サンプルコード(JavaScript)の[Final Result](https://codepen.io/gaearon/pen/gWWZgR?editors=0010)

次は、チュートリアル末尾の追加課題(some ideas for improvements; cf. [Wrapping Up](https://reactjs.org/tutorial/tutorial.html#wrapping-up))に取り組んでみたり、[re-frame](https://github.com/day8/re-frame)を導入してみたり、[re-natal](https://github.com/drapanjanas/re-natal)を試してみたりしても良いかもしれない。

```clj:src/app/core.cljs
(ns app.core
  "This namespace contains your application and is the entrypoint for 'yarn start'."
  (:require
   [reagent.core :as reagent]))

(defn square [& {:keys [value on-click]}]
  [:button.square {:on-click on-click}
   value])

(defn calculate-winner [squares]
  (let [lines [[0 1 2]
               [3 4 5]
               [6 7 8]
               [0 3 6]
               [1 4 7]
               [2 5 8]
               [0 4 8]
               [2 4 6]]]
    (reduce (fn [_ [a b c]]
              (when (and (squares a)
                         (= (squares a) (squares b))
                         (= (squares a) (squares c)))
                (reduced (squares a))))
            nil
            lines)))

(defn board [& {:keys [squares on-click]}]
  (letfn [(render-square [i]
            [square
             :value (squares i)
             :on-click #(on-click i)])]
    [:div
     [:div.board-row
      (render-square 0)
      (render-square 1)
      (render-square 2)]
     [:div.board-row
      (render-square 3)
      (render-square 4)
      (render-square 5)]
     [:div.board-row
      (render-square 6)
      (render-square 7)
      (render-square 8)]]))

(defn game []
  (let [state (reagent/atom {:history [{:squares (vec (repeat 9 nil))}]
                             :x-is-next? true
                             :step-number 0})]
    (letfn [(handle-click [i]
              (let [{:keys [history x-is-next? step-number]} @state
                    history (vec (take (inc step-number) history))
                    current (nth history (dec (count history)))
                    squares (:squares current)]
                (when-not (or (calculate-winner squares)
                              (squares i))
                  (swap! state assoc
                         :history (conj history
                                        {:squares
                                         (assoc squares i (if x-is-next? "X" "O"))})
                         :x-is-next? (not x-is-next?)
                         :step-number (count history)))))
            (jump-to [step]
              (swap! state assoc
                     :x-is-next? (even? step)
                     :step-number step))]
      (fn []
        (let [{:keys [history x-is-next? step-number]} @state
              current (nth history step-number)
              winner (calculate-winner (:squares current))
              status (if winner
                       (str "Winner: " winner)
                       (str "Next player: " (if x-is-next? "X" "O")))
              moves (map-indexed (fn [move _]
                                   (let [desc (if (zero? move)
                                                (str "Go to game start")
                                                (str "Go to move #" move))]
                                     [:li {:key move}
                                      [:button {:on-click #(jump-to move)}
                                       desc]]))
                                 history)]
          [:div.game
           [:div.game-board
            [board
             :squares (:squares current)
             :on-click handle-click]]
           [:div.game-info
            [:div
             status]
            [:ol
             moves]]])))))

(defn ^:dev/after-load render
  "Render the toplevel component for this app."
  []
  (reagent/render [game] (.getElementById js/document "app")))

(defn ^:export main
  "Run application startup logic."
  []
  (render))
```

**実行イメージ(with [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi))**

![実行イメージ(最終)](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33430/685c84e2-6aea-a947-1603-8482efd8ecab.png)

# まとめ

- ClojureScript & Reagentで簡単にReactを始められる!
- ClojureScript楽しい＞ω＜/
- Reagent楽しい＞ω＜/

# Further Reading

- [Reagent](https://github.com/reagent-project/reagent): A minimalistic ClojureScript interface to React.js
- [re-frame](https://github.com/day8/re-frame): A Reagent Framework For Writing SPAs, in Clojurescript.
- [ClojureScript](https://github.com/clojure/clojurescript): Clojure to JS compiler
- [React](https://github.com/facebook/react): A declarative, efficient, and flexible JavaScript library for building user interfaces.
- [ClojureのDuctとClojureScriptのre-frameによるREST API + SPA開発入門](https://qiita.com/lagenorhynque/items/38537fa91300e0ac0070)
- [Clojure/ClojureScript関連リンク集 > Webフロントエンド (ClojureScript)](https://qiita.com/lagenorhynque/items/68c314c288b75a9492ba#web%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%83%89-clojurescript)