Tutorial|Reactでリアルタイムコメントボックスをつくる

Tutorial | Reactを参考にして、リアルタイムコメント機能をつくってみます。翻訳しながら少しずつ進めていきます。


まず適当なエディタを開いて、htmlドキュメントをつくります。

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>React Tutorial</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react-dom.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
  </head>
  <body>
    <div id="content"></div>
    <script type="text/babel" src="scripts/example.js"></script>
    <script type="text/babel">
      // To get started with this tutorial running your own code, simply remove
      // the script tag loading scripts/example.js and start writing code here.
    </script>
  </body>
</html>

以下、このscriptタグの中に、コードを書いていきます。現在はscripts/example.jsを参照しています。
コメント機能が次のような構造をもつように作っていきましょう。

- CommentBox
  - CommentList
    - Comment
  - CommentForm

コメントボックスコンポーネント

// tutorial1.js
var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        Hello, world! I am a CommentBox.
      </div>
    );
  }
});
ReactDOM.render(
  <CommentBox />,
  document.getElementById('content')
);

Note:ネイティブなHTML要素は頭文字が小文字ですが、カスタムReactクラスは頭文字は大文字となります。
まず最初に、JavaScriptの中にあるXML-ish構文に気付くでしょう。Reactには糖衣構文をプレーンなJavaScriptに翻訳するプリコンパイラがあります。糖衣構文(syntactic sugar)とは読み書きをしやすくするために導入する構文のことです。
参考:シンタックスシュガー【syntax sugar】 - インテリジェンス辞書 - Seesaa Wiki(ウィキ)

// tutorial1-raw.js
var CommentBox = React.createClass({displayName: 'CommentBox',
  render: function() {
    return (
      React.createElement('div', {className: "commentBox"},
        "Hello, world! I am a CommentBox."
      )
    );
  }
});
ReactDOM.render(
  React.createElement(CommentBox, null),
  document.getElementById('content')
);

使用は任意ですが、プレーンのJavaScriptより簡単に使えるJSX構文を採用しています。詳細はこちら:JSX in Depth | React

何が起こっているのか?

新しいReactコンポーネントをつくるために、JavaScriptオブジェクトの中のいくつかのメソッドをReact.createClass()に渡しています。メソッドの中でもっとも重要なのはrenderメソッドで、最終的にHTMLにレンダーリングされるReactコンポーネントのツリーを返します。
この

タグは実際のDOMノードではありません。それらはReactのdivコンポーネントインスタンス化です。Reactがどう扱うか知っているデータの標識あるいはピースだと考えることができます。Reactは安全です。HTML文字列を生成しないので、XSS保護は初期設定されます。
基本的なHTMLを返す必要はありません。あなた(あるいは誰か)がつくったコンポーネントのツリーを返すことができます。これがReactを組み立て可能にしています:これは維持可能なフロントエンドの基本教義なのです。
ReactDOM.render()は、ルートコンポーネントインスタンス化し、マークアップを二番目の引数として規定されるraw DOM要素に投入します。
ReactDOMモジュールはDOM特有のメソッドを見えるようにしますが、それに対し、Reactは異なるプラットフォームにおいてReactで共有されるコア・ツールを持ちます。(e.x.,
React Native | A framework for building native apps using React
)

コンポーネントを構成する

コメントリストとコメントフォームの骨格を、再び、シンプルな>|html|

||<タグで作りましょう。コメントボックスの宣言とReactDOM.renderを呼び出してあるファイルにこれら二つのコンポーネントを追加します。

// tutorial2.js
var CommentList = React.createClass({
  render: function() {
    return (
      <div className="commentList">
        Hello, world! I am a CommentList.
      </div>
    );
  }
});

var CommentForm = React.createClass({
  render: function() {
    return (
      <div className="commentForm">
        Hello, world! I am a CommentForm.
      </div>
    );
  }
});

次に、これらの新しいコンポーネントを使うためコメントボックスのコンポーネントをアップデートしましょう。

// tutorial3.js
var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>//追加された
        <CommentList />  //コメントボックスの
        <CommentForm />  //コンポーネント
      </div>
    );
  }
});

どのようにHTMLタグとコンポーネントを融合させているのかに注意しましょう。HTMLコンポーネントは標準のReactコンポーネントで、あなたが定義するものによく似ていますが、ひとつ違いがあります。JSXコンパイラーは自動的にHTMLタグをReact.createElement(tagName)表現に書き換え、そのほかには手を加えません。これはグローバルネームスペースの汚染を防ぐ役割があります。

propsの使用

親から渡されたデータに依存する、コメントコンポーネントをつくりましょう。親コンポーネントから渡されたデータは、子コンポーネント上で'プロパティ'として使用できます。これらの'プロパティ'はthis.propsを通して読み書きされます。propsを使うことで、コメントリストからコメントへ渡されたデータを読むことや、マークアップレンダーリングすることができるようになります。

// tutorial4.js
var Comment = React.createClass({
  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        {this.props.children}
      </div>
    );
  }
});

(属性あるいは子として)JSXの内側で、JavaScript表現を中括弧({})で囲むことで、ツリーの中にテキストやReactコンポーネントを放り込むことができます。this.props上のキーとしてコンポーネントに渡された名前の付けられた属性と、this.props.childrenとしてネスト化された要素にアクセスします。

コンポーネントプロパティ

コメントコンポーネントを定義したので、次は作成者名とコメントのテキストをそこに渡したくなります。これで、それぞれの独立したコメントごとに同じコードを使いまわすことができます。では、コメントリストの中に、いくつかのコメントを追加しましょう。

// tutorial5.js
var CommentList = React.createClass({
  render: function() {
    return (
      <div className="commentList">
        <Comment author="Pete Hunt">This is one comment</Comment>
        <Comment author="Jordan Walke">This is *another* comment</Comment>
      </div>
    );
  }
});

いくつかのデータを親のコメントリストコンポーネントから、子のコメントコンポーネントへ渡したということに注意しましょう。例えば、(属性を通して)Pete Huntと、(XMLライクな子ノードを通して)This is one commentを一番目のコメントに渡しました。上に書いたように、コメントコンポーネントはthis.props.authorと、this.props.childrenを通してこれらの'プロパティ'にアクセスします。

マークダウンを追加する

マークダウンとは、インライン(その場に埋め込まれた)テキストをフォーマットするシンプルな方法です。例えば、アスタリスクでテキストを囲めば、テキストは強調されます。
はじめに、サードパーティのライブラリmarkedをあなたのアプリケーションに追加しましょう。これはJavaScriptのライブラリで、マークダウンを取り込み、raw HTMLに変換します。このライブラリを使うには、(Reactの活動の場にすでに含まれている)headのなかにscriptタグが必要です。

<!-- index.html -->
<head>
  <meta charset="utf-8" />
  <title>React Tutorial</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react-dom.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/marked.min.js"></script><!-- ライブラリmarkedの追加 -->
</head>

次に、コメントテキストをマークダウンに変換して、アウトプットしましょう。

// tutorial6.js
var Comment = React.createClass({
  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        {marked(this.props.children.toString())}//追加
      </div>
    );
  }
});

ここで行っているすべてのことは、markedライブラリの呼び出しです。this.props.childrenを、Reactに包まれたテキストから、markedが理解することになる生の文字列へ変換することが必要なので、はっきりとtoString()を呼び出します。
しかし、ここで問題があります。レンダーリングされたコメントはブラウザで見ると次のように見えます。"

This is another comment

"これらのタグを実際にHTMLとしてレンダーリングしたいところです。
ReactはXSSアタックからあなたをこのように守ります。回避する方法もありますが、フレームワークはそれを使うなと警告します。

// tutorial7.js
var Comment = React.createClass({
  rawMarkup: function() {
    var rawMarkup = marked(this.props.children.toString(), {sanitize: true});
    return { __html: rawMarkup };
  },

  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        <span dangerouslySetInnerHTML={this.rawMarkup()} />
      </div>
    );
  }
});

これは生のHTMLの挿入を故意に難しくする特別なAPIですが、markedに対しては、このバックドア(セキュリティ回避の裏口)をうまく利用します。

この特徴を使うことで、安全性をmarkedに頼っているということを忘れないでください。このケースでは、markedに、ソース内のどんなHTMLマークアップをも回避して、そのまま渡すように伝える、sanitize:true
を渡しています。

データモデルの連結

これまでソースコードに直接コメントを挿入してきました。そのかわり、コメントリストにJSONデータのblobをレンダーリングしてみましょう。最後には、これはサーバーから得られますが、今はコードに書きましょう。

// tutorial8.js
var data = [
  {author: "Pete Hunt", text: "This is one comment"},
  {author: "Jordan Walke", text: "This is *another* comment"}
];

モジュールの方式でコメントリストにこのデータを取り込む必要があります。コメントボックスとReactDOM.render()の呼び出しを修正して、propsを通して、コメントリストの中にこのデータを渡しましょう。

// tutorial9.js
var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.props.data} />
        <CommentForm />
      </div>
    );
  }
});

ReactDOM.render(
  <CommentBox data={data} />,
  document.getElementById('content')
);

さて、今データはコメントリストの中で利用可能になりました。次にコメントを動的にレンダーリングしていきましょう。

// tutorial10.js
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>
    );
  }
});

そうです!

サーバーからフェッチすること

ハードコーディングされたデータとサーバーからの動的なデータを置き換えましょう。データpropを取り除いて、フェッチするためのURLと置き換えます。

// tutorial11.js
ReactDOM.render(
  <CommentBox url="/api/comments" />,
  document.getElementById('content')
);

このコンポーネントは先のものとはことなります。というのも、これは自身を再びレンダーリングしなければならないからです。コンポーネントはリクエストがサーバーから帰ってくるまで、どんなデータも持ちません。その時点で、コンポーネントは新しいコメントをレンダーリングする必要があります。
注意:コードはこの段階では動きません。

リアクティブなstate

これまで、propsに基づいて、それぞれのコンポーネントは自信をレンダーリングしてきました。propsはイミュータブル(イミュータブル(immutable)なオブジェクトとは、作成後にその状態を変えることのできないオブジェクトのこと)です:それらは親から渡され、親に『所有』されます。相互作用を実行するために、コンポーネントにミュータブルなstateを取り入れます。this.stateはコンポーネントに対してはプライベートであり、this.setState()を呼び出すことで変更が可能です。stateがアップデートしたとき、コンポーネントは自信をレンダーリングします。
render()はthis.propsとthis.stateの関数として宣言されたメソッドです。フレームワークはUIが常にインプットと一致することを保証します。
サーバーがデータをフェッチするとき、今あるコメントデータを変更するようにしましょう。コメントデータの配列をコメントボックスコンポーネントにstateとして追加しましょう。

// tutorial12.js
var CommentBox = React.createClass({
  getInitialState: function() {
    return {data: []};
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

getInitialState()は、コンポーネントのライフサイクルの間に正確に一度、実行し、コンポーネントの初期stateを設定します。

stateのアップデート

コンポーネントが最初作られたとき、サーバーからJSONをGETし、最新のデータを反映するためにstateをアップデートします。必要なデータをフェッチするために初期に開始したサーバーに対する非同時性のリクエストをjQueryを使って作りましょう。

[
  {"author": "Pete Hunt", "text": "This is one comment"},
  {"author": "Jordan Walke", "text": "This is *another* comment"}
]
// tutorial13.js
var CommentBox = React.createClass({
  getInitialState: function() {
    return {data: []};
  },
  componentDidMount: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      cache: false,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

コンポーネントレンダーリングされたとき、componentDidMountはReactによって自動的に呼び出されるメソッドです。動的なアップデートの鍵はthis.setState()の呼び出しです。古いコメントの配列を、サーバーからの新しいものに置き換え、UIは自動的にアップデートします。このリアクティビティ(reactivity)のために、ライブアップデートの追加にほんの少しの変更ですみます。シンプルなポーリング(polling)をここで使いますが、簡単にWebSocketsやほかの技術を使うことができるでしょう。

// tutorial14.js
var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      cache: false,
      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 />
      </div>
    );
  }
});

ReactDOM.render(
  <CommentBox url="/api/comments" pollInterval={2000} />,
  document.getElementById('content')
);

ここで行ったのは、AJAXの呼び出しを独立したメソッドに移動させ、コンポーネントが最初にロードされ、その後2秒毎にそれを呼び出すことです。あなたのブラウザでこれを動かして、(サーバーと同じディレクトリの)comments.jsonファイルを変更してみましょう;2秒以内に変化がわかるでしょう!

新しいコメントの追加

では、フォームを作るときです。コメントフォームコンポーネントがユーザーに対して名前とコメントテキストを求め、そのコメントを保存するようサーバにリクエストを送るようにしましょう。

// tutorial15.js
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>
    );
  }
});

フォームをインタラクティブにしましょう。ユーザーがフォームをサブミットしたとき、それをクリアして、サーバーにリクエストをサブミットしてコメントのリストをリフレッシュするようにしましょう。はじめに、フォームのサブミットイベントに耳を傾け、それをクリアしましょう。

// tutorial16.js
var CommentForm = React.createClass({
  handleSubmit: function(e) {
    e.preventDefault();
    var author = this.refs.author.value.trim();
    var text = this.refs.text.value.trim();
    if (!text || !author) {
      return;
    }
    // TODO: send request to the server
    this.refs.author.value = '';
    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>
    );
  }
});

イベント

Reactは、イベントハンドラーを命名規則にcamelCaseを使うコンポーネントに結びつけます。onSubmitハンドラーとフォームを結びつけます。それは、validなインプットがサブミットされたときにフォームのフィールドをクリアにするフォームです。
フォームのサブミット時の、ブラウザのデフォルトのアクションを防ぐために、preventDefault()を呼び出しましょう。

Refs

コンポーネントに名前を割り当てるために、ref属性を使い、DOMノードを参照するためにthis.refsを使います。

Callbacks as props

ユーザーがコメントをサブミットしたとき、新しいコメントを内包させるためコメントのリストをリフレッシュする必要があります。コメントボックスはコメントリストを表示するstateを所有するので、コメントボックス内でこのロジックを行うことは道理にかなっています。
コンポーネントのバックアップからデータをその親に渡します。新しいコールバック(handleCommentSubmit)を子に渡し、それを子のonCommentSubmitイベントと結びつけることによって、親のrenderメソッドの中でこれを行います。いつイベントが起こされても、その度にコールバックは引き起こされます:

// tutorial17.js
var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      cache: false,
      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) {
    // TODO: submit to the server and refresh the list
  },
  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>
    );
  }
});

ユーザーがフォームをサブミットする場であるコメントフォームからコールバックを呼び出しましょう。

// tutorial18.js
var CommentForm = React.createClass({
  handleSubmit: function(e) {
    e.preventDefault();
    var author = this.refs.author.value.trim();
    var text = this.refs.text.value.trim();
    if (!text || !author) {
      return;
    }
    this.props.onCommentSubmit({author: author, text: text});
    this.refs.author.value = '';
    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>
    );
  }
});

コールバックが正しい位置にある今、すべきことはサーバーにサブミットしてリストをリフレッシュすることです:

// tutorial19.js
var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      cache: false,
      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) {
    $.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>
    );
  }
});

最適化:楽観的なアップデート

私たちのアプリケーションは今、フィーチャーコンプリート(基本機能の実装完了)ですが、コメントがリストに表示されるまでリクエストを待たなければいけないことが遅いと感じます。アプリを早く感じさせるために、リストにこのコメントをoptimisticallyに追加できます。

// tutorial20.js
var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      cache: false,
      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>
    );
  }
});

おめでとう!

いくつかのシンプルな段階を経て、コメントボックスを作りあげました。Why React? | Reactについてもっと学んだり、Top-Level API | Reactに飛び込んで、ハッキングを始めましょう!幸運を祈ります!