React.jsのTutorialを試してみる(3


前回に続いて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>
          );
        }
      });

画面表示はこんな感じです。

さらに仕様の追加です。送信したらメッセージをクリアしてリストを更新します。こっから細かいので変更点がわかりづらいと思います。ソースは最後にまとめて載せますが、原文のソースを追いながらのほうがわかりやすいと思います。

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

      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>
          );
        }
      });

ここで重要なのは3点

  • 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

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

        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

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

app.listen(process.env.PORT, process.env.IP);

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

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

最終的な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>