チュートリアル
これから、シンプル且つブログ内に配置可能な実用的なコメント・ボックスを構築します。 Disqus、LiveFyre、またはFacebookコメントによって提供されるようなリアルタイム・コメントの基本的なものになります。
このコメント・ボックスは次の機能を提供します。
- 全てのコメント表示
- コメントを送信するフォーム
- バックエンドのカスタマイズを提供するためのフック
また、幾つかの洗練された機能も持ち合わせます。
チュートリアルをスキップしてソースコードを参照したい場合
GitHub - reactjs/react-tutorial
サーバーの実行
このチュートリアル始めるにあたり、必須ではありませんが、 後ほど実行サーバーへのPOSTが必要になる機能を追加することになります。 もし、あなたが使い慣れた自身のサーバーで構築したいのであれば、それを利用して下さい。 サーバーサイドを気にする事無くReactの学習に集中したい方のために、 我々は様々な言語 - JavaScript(Node.jsを使用)、Python、Ruby、Go、PHP - で書かれたシンプルなサーバーを用意しています。 これらは全てGitHubで利用可能で、ソースコードを見ることも、 始めるためにzipファイルをダウンロードすることも可能です。
チュートリアルを始めるために、まずpublic/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-2.1.3.min.js"></script>
</head>
<body>
<div id="content"></div>
<script type="text/jsx">
// あなたのコードをここに書きます。
</script>
</body>
</html>
以降のこのチュートリアルでは、このscriptタグ内にJavaScriptを書いていくことになります。
注意:
Ajax呼び出しのコードをシンプルにしたいために、jQueryを含めていますが、
Reactを動作させるために強制されるものではありません。
初めてのコンポーネント
Reactはモジューラー、構成可能なコンポーネントが全てです。 このコメント・ボックスの例では、下記のコンポーネント構成を持ちます。
- CommentBox
- CommentList
- Comment
- CommentForm
それでは、<div>だけの、シンプルなコメントボックスのコンポーネントを構築してみましょう。
// tutorial1.js
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
Hello, world! I am a CommentBox.
</div>
);
}
});
React.render(
<CommentBox />,
document.getElementById('content')
);
JSX文法
まず、JavaScriptの中にXML的な文法がある事に気づくでしょう。 このシンタックスシュガーを、下記の素のJavaScriptに変換するシンプルなプリコンパイラが用意されています。
// tutorial1-raw.js
var CommentBox = React.createClass({displayName: 'CommentBox',
render: function() {
return (
React.createElement('div', {className: "commentBox"},
"Hello, world! I am a CommentBox."
)
);
}
});
React.render(
React.createElement(CommentBox, null),
document.getElementById('content')
);
JSXとプリコンパイラの使用は任意ではありますが、素のJavaScriptよりもJSX文法の方が簡単であると自負しています。 詳細については、JSX文法の記事を参照してください。
What's going on
React.createClass()
が新しいReactコンポーネントを作成するために、
JavaScriptオブジェクト内に幾つかのメソッドを渡します。
最も重要なメソッドはrender
であり、最終的にHTMLに描画するReactコンポーネントのツリーを返すために呼び出されます。
<div>
タグは実際のDOMノードでは無く、
Reactのdiv
コンポーネントがインスタンス化されたものになります。
あなたはReactが扱うマークアップまたはデータのことが気になるかもしれませんが、Reactは安全です。
XSSを防御するために、デフォルトでReactはHTML文字列を生成しません。
基本的なHTMLを返す必要は無く、 あなたが構築したコンポーネントのツリーを返すことが出来ます。 これによってReactは構成可能なものとなり、フロントエンドをメンテナンスする際の基本理念となります。
React.render()
はルートコンポーネントをインスタンス化して、フレームワークを開始し、
提供されたマークアップを2つ目の引数として提供された生のDOM要素に注入します。
コンポーネントの構成
再び、シンプルな<div>のCommentListとCommentFormのスケルトンを組み立ててみましょう。
// 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>
);
}
});
次に、これらの新しいコンポーネントを使用するために、CommentBoxコンポーネントを更新します。
// tutorial3.js
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList />
<CommentForm />
</div>
);
}
});
HTMLタグとコンポーネントが混合していることに注目して下さい。
HTMLコンポーネントは、1つの違いを除き、あなたが定義したような通常のReactコンポーネントです。
JSXコンパイラは自動的にHTMLタグをReact.createElement(tagName)
式に書き換え、
その他はそのままにします。(翻訳に自信なし)
これは、グローバルな名前空間の汚染を防ぐために行います。
propsの使用
親から渡されるデータに依存するComment
コンポーネントを作成してみましょう。
親コンポーネントから渡されるデータは、子コンポーネント上で'プロパティ'として利用可能です。
これらの'プロパティ'はthis.props
を通してアクセス可能です。
propsを使用することで、CommentList
からComment
に渡されるデータを読み込み、
マークアップを描画することが可能になります。
// 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
とする入れ子の要素にアクセスします。
コンポーネントのプロパティ
ここまでで、Comment
コンポーネントが定義されたので、
投稿者名(author)とコメントテキストを渡してみましょう。
こうすることで、各コメントで重複するコードの再利用を可能にしてくれます。
それでは、CommentList
にコメントを追加してみましょう。
// 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>
);
}
});
マークダウンの追加
マークダウン記法は、テキストをインラインでフォーマットするための簡単な方法です。 例えば、アスタリスクでテキストを囲むと、そのテキストは強調されます。
まず、サードパーティのライブラリをアプリケーションに追加します。 これはマークダウンのテキストを受け取って生のHTMLに変換するJavaScriptのライブラリです。 head内にscriptタグが必要になります。(Reactのプレイグラウンドには既に含まれています。)
<!-- index.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/marked/0.3.2/marked.min.js"></script>
</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ライブラリの呼び出しです。
Reactにラップされたテキストから、this.props.children
を生の文字列に変換して、
markedに理解させる必要があるため、明示的にtoString()
を呼び出します。
ただし、これには問題があります。 描画されたコメントはブラウザ上で「"<p>This is <em>another</em> comment</p>"」のように表示されます。 これらをHTMLとして正しく描画させる必要があります。
これは、ReactがXSSによる攻撃を防ぐためです。 これを回避する方法は存在しますが、フレームワークが警告を発することになります。
// tutorial7.js
var Comment = React.createClass({
render: function() {
var rawMarkup = marked(this.props.children.toString(), {sanitize: true});
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
<span dangerouslySetInnerHTML={{__html: rawMarkup}} />
</div>
);
}
});
これは意図的に生のHTMLを挿入することを困難にする特別なAPIですが、 markedのためにこのバックドアを利用します。
注意:
この機能を使用することで、markedにソースを依存させることになる事を覚えておいて下さい。
このケースでは、変更無しに渡す代わりに、
markedにsanitize: true
を渡すことで、
ソース内のHTMLのマークアップをエスケープするように伝えます。
データモデルとの連携
ここまでに、ソースコードに直接コメントを挿入してきました。 代わりに、JSONデータをコメントリストに入れて描画してみましょう。 最終的にはサーバーから取得したものを使用しますが、 今のところは下記のようにしてJSONデータを用意します。
// tutorial8.js
var data = [
{author: "Pete Hunt", text: "This is one comment"},
{author: "Jordan Walke", text: "This is *another* comment"}
];
CommentList
に入れるこのデータの取得は、モジュール形式にする必要があります。
propsを介してこのデータをCommentList
に渡すために、
CommentBox
とこのReact.render()の呼び出し
を変更します。
// tutorial9.js
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.props.data} />
<CommentForm />
</div>
);
}
});
React.render(
<CommentBox data={data} />,
document.getElementById('content')
);
CommentList
内でデータが利用可能になったので、
直接コメントを描画してみましょう。
// 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
React.render(
<CommentBox url="comments.json" />,
document.getElementById('content')
);
このコンポーネントは以前のコンポーネントとは自身で再描画しなければいけないため、 以前のコンポーネントとは異なります。 コンポーネントは、サーバーに対してリクエストを送り、データが返されるまではデータを持つことが出来ず、 その時点では新しいコメントを描画する必要があるかもしれません。
Reactiveなstate
これまでに、各コンポーネントはpropsを基に自身を描画していました。
props
は不変(immutable)です。親から"親が所有する"ものとして渡されます。
データを双方で変更出来るように、変更(mutable)状態をコンポーネントに導入します。
this.state
はコンポーネントでプライベートなものであり、
this.setState()
の呼び出しによって変更することが可能です。
state
が更新されると、コンポーネントは自分自身を再描画します。
render()
メソッドは、
this.props
とthis.state
の命令文/処理文(declaratively)のように書かれます。
このフレームワークは、UIが入力と一致している事を保証します。
サーバーがデータを取得すると、コメントデータが変更される必要があります。
コメントのデータの配列をCommentBox
コンポーネントに、
その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の更新
コンポーネントが最初に作られる際に、 サーバーからJSONをGETして、状態(state)を更新して最新のデータを反映する必要があります。 実際のアプリケーションではこれは動的なエンドポイントになりますが、 この例では静的なJSONファイルを使用して、シンプルな状態を維持します。
// tutorial13.json
[
{"author": "Pete Hunt", "text": "This is one comment"},
{"author": "Jordan Walke", "text": "This is *another* comment"}
]
サーバーへの非同期リクエストには、jQueryを使用します。
注意:
AJAXアプリケーションの作成に入っていくため、ファイルシステム上では無く、
Webサーバーを使用した開発が必要になります。
前述したように、我々はGitHub上で、
あなたが使用することの出来る幾つかのサーバーのプログラムを提供しています。
これらは、この後のチュートリアルで必要となる環境を提供します。
// tutorial13.js
var CommentBox = React.createClass({
getInitialState: function() {
return {data: []};
},
componentDidMount: 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)
});
},
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm />
</div>
);
}
});
componentDidMount
は、Reactによってコンポーネントが描画された際に自動的に呼び出されるメソッドです。
動的に更新するには、this.setState()
を呼び出します。
サーバーから取得したものをコメントの古い配列と置換え、UIは自動的に自身の更新を行います。
このようにReactivity(反応的)であるため、これがライブ更新(live updates)を追加する唯一の変更になります。
ここではシンプルなポーリングを使用しますが、実践であればWebSocketsなどのテクノロジーを使用するのが良いでしょう。
// tutorial14.js
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)
});
},
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>
);
}
});
React.render(
<CommentBox url="comments.json" pollInterval={2000} />,
document.getElementById('content')
);
ここでは、AJAX呼び出しを別のメソッドに移動して、
コンポーネントの最初の読み込み時とその後2秒毎に呼び出すようにしました。
ブラウザ上で実行して、comments.json
ファイルを変更してみましょう。
2秒後に変更されたものが表示されるはずです。
新しいコメントの追加
ここからは、フォームの構築をしていきます。
CommentForm
コンポーネントは、ユーザーに彼らの名前とコメントテキストの入力を促し、
サーバーにリクエストを送り、そのコメントを保存します。
// 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 = React.findDOMNode(this.refs.author).value.trim();
var text = React.findDOMNode(this.refs.text).value.trim();
if (!text || !author) {
return;
}
// TODO : 後でサーバーへのリクエスト処理を実装する
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>
);
}
});
// tutorial17.js
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) {
// TODO : サーバーへ送信し、リストをリフレッシュする
},
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>
);
}
});
ユーザーがフォームを送信した際に、
CommentForm
からコールバックを呼び出すようにしましょう。
// tutorial18.js
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>
);
}
});
コールバックが適切な場所に配置されたので、 サーバーへの送信とリストのリフレッシュの処理を実装しなければいけません。
// tutorial19.js
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) {
$.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>
);
}
});
最適化: 楽観的な更新
アプリケーションの必要な機能は一通り揃いましたが、コメントがリスト内に表示される直前、 リクエストが完了するまで待たされるため遅く感じてしまいます。 コメントのリストへの追加を"楽観的"にすることで、 アプリケーションの動作を速く感じるようにします。
// tutorial20.js
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>
);
}
});
Congrats!
少しの手順でコメントボックスを構築することが出来ました。 更なる学習のために、何故Reactを使用するのかについて学び、 APIリファレンスを読んでハッキングしてみて下さい!グッドラック!