Clojure Web Development勉強 - ReactチュートリアルをReagentでやってみる(その2)
React公式チュートリアルを進める。今回はPassing Data Through PropsからLifting State Upまで。
Passing Data Through Props
https://facebook.github.io/react/tutorial/tutorial.html#passing-data-through-props
親コンポーネント(つまり呼び出し側のコンポーネント)から子コンポーネントにデータを渡す方法について。
ReactというかJSXだと、<Square />
の中にx={y}
のような代入式を入れることによってSquareコンポーネントにデータを送る:
class Board extends React.Component { renderSquare(i) { return <Square value={i} />; } ... }
Reagentだと[Square]
のようなVectorの第二要素以降(例えば[Square y]
)にするだけでよい:
(defn Board [] (let [renderSquare (fn [i] [Square i]) ...] ...))
Reactの場合、受けとる側の子コンポーネントは、渡されたデータにthis.props
というオブジェクトを通じてアクセスできる:
class Square extends React.Component { render() { return ( <button className="square"> {this.props.value} </button> ); } }
Reagentだと、明示的に引数として定義して、その引数名でアクセスする:
(defn Square [i] [:button.square i])
これでとりあえず0〜8までの数字が順番にSquareに表示されるようになっている。
個人的にはReagentの明示的な引数という方式は非常に好ましい。JavaScriptの方式だと、親コンポーネントはthis.props
というオブジェクトにどんな名前のプロパティでもつけられ、子コンポーネントも何のプロパティを使ってもよく、子コンポーネントがどこで何のプロパティをどんな名前で使っているかを把握した上で親コンポーネントを書かなければいけなく、Zen of Pythonじゃないけど"explicit is better than implicit"と言いたくなる。
あといちいち{this.props.value}
とかvalue={i}
とか書かなくていいのも嬉しい。
An Interactive Component
https://facebook.github.io/react/tutorial/tutorial.html#an-interactive-component
クリックに反応する挙動を実装していく。
まずはクリックするとJavaScriptのポップアップアラートが開くような機能を試しにつけてみる。
JavaScriptだとonClickプロパティに匿名関数をくっつける:
<button className="square" onClick={() => alert('click')}>
Reagentでも似たようなもの:
[:button.square {:on-click #(js/alert "click")}]
これで「クリックするとアラート」という機能は実装できた。
次は「クリックすると×マークがつく」という機能。Squareコンポーネントに状態を持たせてみる:
class Square extends React.Component { constructor() { super(); this.state = { value: null, }; } render() { return ( <button className="square" onClick={() => this.setState({value: 'X'})}> {this.state.value} </button> ); } }
Squareコンポーネントにコンストラクタを定義し、そこでthis.state.value
をnullに初期化。その値を常に表示するようにし、クリックイベントでthis.state.value
の値を"X"に変更する。変更された時点で表示も正しく更新される。
Clojureだとこう:
(defn Square [i] (let [state (r/atom nil)] (fn [i] [:button.square {:on-click #(reset! state "X")} @state])))
this.state.value
ではなく、コンポーネントにクロージャで束縛されているstate atomにデータを保持させる。クリックで更新、常に最新の値を表示させる、というのはJavaScript版とまったく同じ。
これでSquareの一つをクリックするとそこに×マークがつく。
Lifting State Up
https://facebook.github.io/react/tutorial/tutorial.html#lifting-state-up
Squareに状態持たせると勝ち負け判定とかできないよね、状態はすべてBoardに引き上げてしまってSquareはステートレスに戻そう、という方向で実装していく。
まずBoardコンポーネント側のJavaScript:
class Board extends React.Component { constructor() { super(); this.state = { squares: Array(9).fill(null), }; } handleClick(i) { const squares = this.state.squares.slice(); squares[i] = 'X'; this.setState({squares: squares}); } renderSquare(i) { return <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} />; } }
this.state.squares
という状態を持たせるためのコンストラクタと、その状態をアップデートするためのhandleClick
メソッド関数と、this.state.squares
の一部の値とその一部を変更できるhandleClick
関数をSquareのthis.props
に渡すrenderSquare
メソッド関数を定義している。
Clojureではすべてletで定義・束縛していく:
(defn Board [] (let [squares (r/atom (apply vector (repeat 9 nil))) handle-click #(swap! squares assoc % "X") renderSquare (fn [i] [Square (get @squares i) #(handle-click i)]) ...] ...))
squaresはreagent atom。handle-clickはそのatomをクロージャに持ち、変更できる関数。renderSquareでSquareに引数としてsquaresの中のi番目の値と、#(handle-click i)
という匿名関数を渡してやる。
Squareコンポーネントはthis.state
を消し、データはthis.props
からとるようにする。this.props.onClick
の中身がBoardコンポーネントのメソッドなおかげでBoardのthis.state.squares
を変更できる。
class Square extends React.Component { render() { return ( <button className="square" onClick={() => this.props.onClick()}> {this.props.value} </button> ); } }
Reagentで実装するとこんな感じ:
(defn Square [value click-fn] [:button.square {:on-click click-fn} value])
明示的かつ簡潔。
挙動は「An Interactive Component」で実装したものと同一。ただし、squaresがBoardレベルで定義されていることで、これをチェックすれば勝ち負け判定などができるようになった。
ここまでの感想
JavaScriptのほうはボイラープレートの多さもさることながら、暗黙のうちに定義されているものが多い印象。
例えばthis.props
とかその中のフィールドとかは唐突に「存在するもの」として子コンポーネントのコードで使われる。あとthis.setState
とかも定義されずに出現する。
ボイラープレートが減るのはいいことなのだけど、同じ命令的オブジェクト指向言語でもPythonに慣れ親しんだ身としては「明示的であることは犠牲にしないでくれ!」と言いたくなる。慣れの問題も大きいのだろうけど。
そういう意味では、ClojureはPythonとの文化的・思想的な距離は意外と近い(ところもある)のかもしれない。
次回もチュートリアルを進めていく。
今回の終わりの時点でのコード(core.cljsのみ。他のファイルは変更なし):