React.jsのTutorialを試してみる（３

[前回](http://qiita.com/kazusa-qooq/items/61ee2b29d1bcc241bbe4)に続いてReact.jsのTutorialを試していきます。思ってたより、長い。

##Adding new comments
ようやく投稿フォームの登場です。名前とコメントを入力して送信する仕様みたいです。
CommentFormを変更します。

```
      var CommentForm = React.createClass({
        render: function() {
          return (
            <form className="commentForm">
              <input type="text" placeholder="Your name" />
              <input type="text" placeholder="Say something..." />
              <input type="submit" value="Post" />
            </form>
          );
        }
      });
```
画面表示はこんな感じです。
![image](https://qiita-image-store.s3.amazonaws.com/0/71988/e830d3f4-c0fc-3a2c-e4d0-ac0173394c3a.png)
さらに仕様の追加です。送信したらメッセージをクリアしてリストを更新します。こっから細かいので変更点がわかりづらいと思います。ソースは最後にまとめて載せますが、原文のソースを追いながらのほうがわかりやすいと思います。

まずはメッセージのクリアからです。

```
      var CommentForm = React.createClass({
        handleSubmit: function(e) {
          e.preventDefault();
          var author = React.findDOMNode(this.refs.author).value.trim();
          var text = React.findDOMNode(this.refs.text).value.trim();
          if (!text || !author) {
            return;
          }
          // TODO: send request to the server
          React.findDOMNode(this.refs.author).value = '';
          React.findDOMNode(this.refs.text).value = '';
          return;
        },
        render: function() {
          return (
            <form className="commentForm" onSubmit={this.handleSubmit}>
              <input type="text" placeholder="Your name" ref="author" />
              <input type="text" placeholder="Say something..." ref="text" />
              <input type="submit" value="Post" />
            </form>
          );
        }
      });
```

ここで重要なのは３点

* onSubmitのようにcamelCaseのハンドラが用意される
* ref属性で指定した名前でrefsからcomponentを参照できる
* React.findDOMNode(component)でNative DOMにアクセスできる

以上をもとにonSubmitハンドラをトリガーにして、refsとReact.findDOMNodeからinputのDOM elementを取得し、値をクリアします。

つづいてリストの更新ですが、CommentListとCommentFormは兄弟関係にあり親を経由しないとイベントを通知できません。そこでFormに`onCommentSubmit`ハンドラを用意し、BoxはそれをトリガーにListを更新します。
まずBox側でFormにハンドラを作成します。

```
      var CommentBox = React.createClass({
        ...
        handleCommentSubmit: function(comment) {
          // TODO: submit to the server and refresh the list
        },
        ...
        render: function() {
          return (
            <div className="commentBox">
              ...
              <CommentForm onCommentSubmit={this.handleCommentSubmit} />
            </div>
          );
        }
      });
```
つぎにForm側でsubmit時にハンドラを呼び出します。

```
      var CommentForm = React.createClass({
        handleSubmit: function(e) {
         ...
         this.props.onCommentSubmit({author: author, text: text});
         ...
      });
```

つぎにBoxのhandleCommentSubmitに送信処理をajaxで実装します。しかし受信側のURLが現状静的ファイルのため、送信しても見た目上変化はありません。

```
  handleCommentSubmit: function(comment) {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      type: 'POST',
      data: comment,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
```

##Optimization: optimistic updates
ここで最初の目標にあげていたＵＸ向上のための処理：サーバからの返答を待たずにリストに反映を追加します。一瞬反映されると思いますが、その後、静的ファイルがレスポンスで帰ってくるので元に戻ると思います。

```
        handleCommentSubmit: function(comment) {
          var comments = this.state.data;
          var newComments = comments.concat([comment]);
          this.setState({data: newComments});
```

##Congrats!
チュートリアルとしてはここで終わりです。やっぱりちゃんと動かしたいので、用意されたソースを使ってサーバサイドも動かしたいと思います。下記zipを解凍し、npm install してください。
https://github.com/reactjs/react-tutorial/archive/master.zip

```
:~/workspace/react/react-tutorial-master (master) $ npm install
body-parser@1.12.2 node_modules/body-parser
├── content-type@1.0.1
├── bytes@1.0.0
├── raw-body@1.3.3
├── depd@1.0.0
├── qs@2.4.1
├── iconv-lite@0.4.7
├── on-finished@2.2.0 (ee-first@1.1.0)
├── debug@2.1.3 (ms@0.7.0)
└── type-is@1.6.1 (media-typer@0.3.0, mime-types@2.0.10)

express@4.12.3 node_modules/express
├── merge-descriptors@1.0.0
├── utils-merge@1.0.0
├── methods@1.1.1
├── cookie@0.1.2
├── fresh@0.2.4
├── cookie-signature@1.0.6
├── escape-html@1.0.1
├── range-parser@1.0.2
├── content-type@1.0.1
├── finalhandler@0.3.4
├── vary@1.0.0
├── parseurl@1.3.0
├── serve-static@1.9.2
├── content-disposition@0.5.0
├── path-to-regexp@0.1.3
├── depd@1.0.0
├── on-finished@2.2.0 (ee-first@1.1.0)
├── qs@2.4.1
├── debug@2.1.3 (ms@0.7.0)
├── etag@1.5.1 (crc@3.2.1)
├── send@0.12.2 (destroy@1.0.3, ms@0.7.0, mime@1.3.4)
├── proxy-addr@1.0.7 (forwarded@0.1.0, ipaddr.js@0.1.9)
├── type-is@1.6.1 (media-typer@0.3.0, mime-types@2.0.10)
└── accepts@1.2.5 (negotiator@0.5.1, mime-types@2.0.10)

```
入りました。cloud9上で動かす場合
server.jsの42行目を以下のように変更してください。

```
app.listen(process.env.PORT, process.env.IP);
```
その後、public/index.htmlを作成したものに置き換えましょう。置き換えなくても動きますけど、置き換えましょう。
最後に`npm start`でサーバを起動させ、ファイルにアクセスします。ブラウザを複数開けば同期しているのがわかると思います。

サーバサイドの処理を詳しく知りたい方はserver.jsをみるといいと思います。
４０行くらいで、ファイルに読み書きしているようです。

最終的なindex.htmlを最後に載せておきます。

```
<!-- index.html -->
<html>
  <head>
    <title>Hello React</title>
    <script src="https://fb.me/react-0.13.1.js"></script>
    <script src="https://fb.me/JSXTransformer-0.13.1.js"></script>
    <script src="https://code.jquery.com/jquery-1.10.0.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js"></script>
  </head>
  <body>
    <div id="content"></div>
    <script type="text/jsx">
      var data = [
        {author: "Pete Hunt", text: "This is one comment"},
        {author: "Jordan Walke", text: "This is *another* comment"}
      ];
      var converter = new Showdown.converter();
      var Comment = React.createClass({
        render: function() {
          var rawMarkup = converter.makeHtml(this.props.children.toString());
          return (
            <div className="comment">
              <h2 className="commentAuthor">
                {this.props.author}
              </h2>
              <span dangerouslySetInnerHTML={{__html: rawMarkup}} />
            </div>
          );
        }
      });
      
      var CommentList = React.createClass({
        render: function() {
          var commentNodes = 
                this.props.data.map(function (comment) {
            return (
              <Comment author={comment.author}>
                {comment.text}
              </Comment>
            );
          });
          return (
            <div className="commentList">
              {commentNodes}
            </div>
          );
        }
      });
      
      var CommentForm = React.createClass({
        handleSubmit: function(e) {
          e.preventDefault();
          var author = React.findDOMNode(this.refs.author).value.trim();
          var text = React.findDOMNode(this.refs.text).value.trim();
          if (!text || !author) {
            return;
          }
          this.props.onCommentSubmit({author: author, text: text});
          React.findDOMNode(this.refs.author).value = '';
          React.findDOMNode(this.refs.text).value = '';
          return;
        },
        render: function() {
          return (
            <form className="commentForm" onSubmit={this.handleSubmit}>
              <input type="text" placeholder="Your name" ref="author" />
              <input type="text" placeholder="Say something..." ref="text" />
              <input type="submit" value="Post" />
            </form>
          );
        }
      });
      
      var CommentBox = React.createClass({
        loadCommentsFromServer: function() {
          $.ajax({
            url: this.props.url,
            dataType: 'json',
            success: function(data) {
              this.setState({data: data});
            }.bind(this),
            error: function(xhr, status, err) {
              console.error(this.props.url, status, err.toString());
            }.bind(this)
          });
        },
        handleCommentSubmit: function(comment) {
          var comments = this.state.data;
          var newComments = comments.concat([comment]);
          this.setState({data: newComments});
          $.ajax({
            url: this.props.url,
            dataType: 'json',
            type: 'POST',
            data: comment,
            success: function(data) {
              this.setState({data: data});
            }.bind(this),
            error: function(xhr, status, err) {
              console.error(this.props.url, status, err.toString());
            }.bind(this)
          });
        },
        getInitialState: function() {
          return {data: []};
        },
        componentDidMount: function() {
          this.loadCommentsFromServer();
          setInterval(this.loadCommentsFromServer, this.props.pollInterval);
        },
        render: function() {
          return (
            <div className="commentBox">
              <h1>Comments</h1>
              <CommentList data={this.state.data} />
              <CommentForm onCommentSubmit={this.handleCommentSubmit} />
            </div>
          );
        }
      });
      
      React.render(
        <CommentBox url="comments.json" pollInterval={2000} />,
        document.getElementById('content')
      );
    </script>
  </body>
</html>
```