JavaScript
Clojure
ClojureScript
React
Reagent

ClojureScript & ReagentでReact入門してみた

そろそろReactに入門したいなぁ、でもClojurianとしてはJavaScriptやTypeScriptではなくClojureScriptで書きたいなぁ、ということでClojureScriptとReagentReact公式チュートリアルに取り組んでみた。

最終的なReactチュートリアルアプリ(tic-tac-toe; 三目並べ)の実装例はこちら:

lagenorhynque/react-tutorial


React->Reagentのポイント

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

React
Reagent

JSX (React element)

Hiccup風のDSL

React component
ClojureScriptの関数

props
コンポーネント関数の引数

state
reagent.core/atom


ClojureScript/Reagentの始め方


プロジェクト生成

簡単に始めるために、ここではClojureバックエンドなしのreagent-frontendテンプレートでClojureScriptプロジェクトを生成する(※ もちろんLeiningenは事前にインストールしておく)。

$ lein new reagent-frontend react-tutorial

生成されるプロジェクトの構成は以下の通り。

ここでは、直接 src/react_tutorial/core.cljs にチュートリアルアプリを実装してみる。

$ tree react-tutorial/

react-tutorial/
├── LICENSE
├── README.md
├── env
│   ├── dev
│   │   └── cljs
│   │   └── react_tutorial
│   │   └── dev.cljs
│   └── prod
│   └── cljs
│   └── react_tutorial
│   └── prod.cljs
├── project.clj
├── public
│   ├── css
│   │   └── site.css
│   └── index.html
└── src
└── react_tutorial
└── core.cljs

11 directories, 8 files


テスト起動

ちなみに、この時点で lein-figwheel を利用してアプリを起動してみると、 http://localhost:3449/ に自動生成された初期画面が開く。

$ cd react-tutorial/

$ lein figwheel

実行イメージ(with React Developer Tools)

実行イメージ(初期)


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

最後に、チュートリアルのStarter Codeを参考に、土台となるHTMLの差分を public/index.html に、CSSを public/css/site.css に反映すれば準備完了。


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

Reagent公式ページdocを参考に、まずはReagentでReactコンポーネントを作ってみる。


コンポーネントの定義

例えば、Starter CodeにあるReactコンポーネント Game は、JavaScriptで以下のように書かれている。

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スタイルのDSL(ClojureScriptのVector, Map, Keyword, etc.)でHTMLを表現できる。

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

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

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

(defn game []

[:div.game
[:div.game-board
[board]]
[:div.game-info
[:div
;; status
]
[:ol
;; TODO
]]])


コンポーネントの利用

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

<Board />

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

[board]

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

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


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


props の導入

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

例えば、Passing Data Through PropsSquare コンポーネントは props を利用して以下のように書かれている。

class Square extends React.Component {

render() {
return (
<button className="square">
{this.props.value}
</button>
);
}
}

これをReagentで書くと、

(defn square [& {:keys [value]}]

[:button.square
value])

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


function components

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

ここまでのReagentでのコンポーネント定義方法(A Simple Function)はちょうどそれに相当するものだといえる。

※ Reactのclassによるコンポーネント定義に相当する定義方法はこちら: A Class With Life Cycle Methods

Function Componentsにある Square コンポーネントの最終形も以下のように実装できる。

function Square(props) {

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

↓↓↓

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

[:button.square {:on-click on-click}
value])

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

<Square

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

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

[square

:value (squares i)
:on-click #(on-click i)]

のように書ける。


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


state の導入

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

Making an Interactive Componentでは、 Square コンポーネントが内部状態として state.value を持ち、buttonのonClickで null から 'X' に更新する。

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 を利用する。

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

atom による状態を def でnamespaceのトップレベルに定義することもできるが、ここではコンポーネントの個々のインスタンスにローカルな内部状態として持たせたいので、 atom を参照するクロージャを返す方法(A Function Returning A Function)を利用する。

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

(ns react-tutorial.core

(: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公式ページも参照しつつReactチュートリアルを進めてみると、ClojureScriptとReagentでシンプルにチュートリアルアプリが実装できた。

lagenorhynque/react-tutorial

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

cf. 公式サンプルコード(JavaScript)のFinal Result

次は、チュートリアル末尾の追加課題(some ideas for improvements; cf. Wrapping Up)に取り組んでみたり、re-frameを導入してみたり、re-natalを試してみたりしても良いかもしれない。


src/react_tutorial/core.cljs

(ns react-tutorial.core

(:require [reagent.core :as reagent]))

;; -------------------------
;; Views

(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 [history (:history @state)
current (nth history (:step-number @state))
winner (calculate-winner (:squares current))
status (if winner
(str "Winner: " winner)
(str "Next player: "
(if (:x-is-next? @state) "X" "O")))
moves (map-indexed (fn [move _]
(let [desc (if (zero? move)
(str "Game start")
(str "Move #" move))]
[:li {:key move}
[:a {:href "#"
: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 home-page []
[game])

;; -------------------------
;; Initialize app

(defn mount-root []
(reagent/render [home-page] (.getElementById js/document "app")))

(defn init! []
(mount-root))


実行イメージ(with React Developer Tools)

実行イメージ(最終)


まとめ


  • ClojureScript & Reagentで簡単にReactを始められる!

  • ClojureScript楽しい>ω</

  • Reagent楽しい>ω</


Further Reading



  • Reagent: A minimalistic ClojureScript interface to React.js


  • re-frame: A Reagent Framework For Writing SPAs, in Clojurescript.


  • ClojureScript: Clojure to JS compiler


  • React: A declarative, efficient, and flexible JavaScript library for building user interfaces.