読者です 読者をやめる 読者になる 読者になる

Arantium Maestum

プログラミング、囲碁、読書の話題

Clojure Web Development勉強 - ReactチュートリアルをReagentでやってみる(その2)

Clojure Clojurescript Web React Reagent

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に慣れ親しんだ身としては「明示的であることは犠牲にしないでくれ!」と言いたくなる。慣れの問題も大きいのだろうけど。

そういう意味では、ClojurePythonとの文化的・思想的な距離は意外と近い(ところもある)のかもしれない。

次回もチュートリアルを進めていく。

今回の終わりの時点でのコード(core.cljsのみ。他のファイルは変更なし):

gist.github.com