5分で理解する React.js

いまさらですがReact(react.js)をはじめてみました。

Virtual DOMばかりが話題にあがるReactですが、それにとらわれていると理解が進まない、と言うかReactで理解しなければならないのはVirtual DOMではないことがわかりました。

Reactについての良い資料はすでにたくさんありますので、末尾に参考資料としてあげています。

このエントリは自分がReactのチュートリアルをなぞりながら書いた自分用のメモですが「1エントリで概要をちゃちゃっと理解したい」という方に役に立ったら良いな、とも思っています。

Reactチュートリアル

http://facebook.github.io/react/docs/tutorial.html


基本


  • JavaScriptで書かれたライブラリ。react.jsをインクルードして使う。

  • (MVCで言うところの)Viewのみを担当する。

  • JavaScriptのコード中に(PHPの様に)「HTMLタグ(っぽいもの)」を書ける。

return (

<div className="commentForm">
Hello, world! I am a CommentForm.
</div>
);


  • この「HTMLタグ(っぽいもの)」はJavaScriptのシンタックスではエラーになるので実行する前にJavaScriptに変換する。

  • 変換は実行直前の自動変換も、事前の静的な変換も可能。前者はJSXTransformer.jsをインクルードすることで、後者はjsxコマンドで実施。

  • jQueryと共存できる。


コンポーネント

React.js ではコンポーネントを定義し、それを組み合わせることで画面を作る。


コンポーネントの定義

コンポーネントはこんな風に作る。

最低限renderメソッドを実装すればOK。

この例ではCommentBoxというコンポーネントを作っている。

var CommentBox = React.createClass({

render: function() {
return (
<div className="commentBox"> // … ①
Hello, world! I am a CommentBox.
</div>
);
}
});

①はあくまでも「HTMLっぽいもの」なので、ちょっとHTMLと違う。

HTMLで言うところのclassclassNameと書く。

renderメソッドは「ピュア」である必要がある。これは、「コンポーネントの状態を操作しない」「DOMの操作(読み書き)をしない」「ブラウザの操作をしない」ということ。

ちなみに上記では1つのdivコンポーネントを返しているが、コンポーネントの中にコンポーネントを含めることも可能。ただし、ルート要素(上記の場合div)は1つしか返せない。

つまり、以下の様な返し方はNG。

// NG例

return (
<div className="commentBox">
Hello, world! I am a 1st CommentBox.
</div>
<div className="commentBox">
Hi, world! I am a 2nd CommentBox.
</div>
);


コンポーネントの使用

作ったコンポーネントはこんな風に使う。

これでHTMLの content というIDを持つ要素に CommentBox コンポーネントが描画される。

React.render(

<CommentBox />,
document.getElementById('content')
);


パラメタ(props)

コンポーネント使用時にパラメタを渡すことができる。

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

ここでは、Commentというコンポーネントにauthorというパラメタを渡している。

この様に渡されたパラメタはコンポーネント側ではthis.propsオブジェクト経由で参照できる。

また、コンポーネントの開きタグ(<Comment>)と閉じタグ(</Comment>)の間の文字列はthis.props.childrenオブジェクトで参照する。

var Comment = React.createClass({

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

this.props.childrenそのものはJavaScriptの文字列ではないので、JavaScriptの文字列が必要になる局面では.toString()メソッドを使う。

{marked(this.props.children.toString())}


createClassのメソッド

コンポーネントを定義するcreateClassはいくつかのメソッドを持っている。


getInitialState


  • コンポーネントの初期化時に1回だけ実行される。

  • 返した値はthis.stateの初期値として使用される。


componentDidMount


  • 初回の描画(render)の後に実行される。

  • このメソッドが実行される時、React的なDOMは構築済。

  • 子コンポーネントがある場合、子コンポーネントのcomponentDidMountが先に実行される。

  • タイマーでポーリングするためにsetIntervalする場合はこのメソッドの中でする。


その他のメソッド

Component Specs and Lifecycle

https://facebook.github.io/react/docs/component-specs.html


サーバからデータを取得するサンプル

下のサンプルコードはこの様に動く:



  1. getInitialState が実行されてthis.stateが初期化される。


  2. renderが実行される。この状態ではCommentListモジュールには初期化された空のthis.state.dataが渡される。


  3. componentDidMountが実行される。この中でjQueryを使ってサーバAPIからJSONを取得、this.setState()を使って取得したデータをコンポーネントにセットする。


  4. this.setState()によって再度renderが実行される。この状態ではCommentListモジュールにサーバAPIから取得したデータが渡される。

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();
},
render: function(){
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm />
</div>
);
}
});
React.render(
<CommentBox url="/path/to/api.json" />,
document.getElementById('content')
);


サーバにデータを送信するサンプル

propsを使用したコンポーネント間のデータ受け渡しは親から子への受け渡しだった。

以下の様な構造のコンポーネントを想定する。

comment-box.png

CommentListにはコメントの一覧が表示されていて、CommentFormには入力欄や送信ボタンが付いている。

CommentFormにコメントを記入して送信ボタンを押した時にCommentListにそれを反映したい、という様な場合では、親から子への受け渡しだけでは実現できない。

こんな時は親コンポーネントに定義したメソッドを、props経由で実行して親コンポーネントの状態を変更し、親コンポーネント経由でCommentListの状態を変える。

ここでは以下の方法で実装している。


  • 親コンポーネントにhandleCommentSubmitを定義する。(1)

  • 親コンポーネントのrenderメソッドで子コンポーネントを使用する時にpropsとしてonCommentSubmit={this.handleCommentSubmit}を渡す。(2)

  • 子コンポーネントにhandleSubmitメソッドを定義する(3)。この中でprops経由で親コンポーネントのonCommentSubmitメソッドを実行する。(4)

  • 子コンポーネントでフォームを定義する際に<form className="commentForm" onSubmit={this.handleSubmit}>としてフォームサブミット時にhandleSubmitを実行する。(5)

// 親コンポーネント

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) { // … 1
// 自身の状態を変更する
// 良好な操作感のためにサーバに送信する前に先行して変更してしまう。
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} />
// 子コンポーネントからonCommentSubmitを実行された時にhandleCommentSubmitを実行する
<CommentForm onCommentSubmit={this.handleCommentSubmit} /> // 2
</div>
);
}
});
// tutorial2.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>
);
}
});

// 子コンポーネント
var CommentForm = React.createClass({
// フォームをサブミットした時に実行される
handleSubmit: function(e) { // … 3
e.preventDefault();
var author = React.findDOMNode(this.refs.author).value.trim();
var text = React.findDOMNode(this.refs.text).value.trim();
if (!text || !author) {
return;
}
// 親コンポーネントのonCommentSubmitを実行する
this.props.onCommentSubmit({author: author, text: text}); 4
React.findDOMNode(this.refs.author).value = '';
React.findDOMNode(this.refs.text).value = '';
return;
},
render: function() {
return (
// フォームサブミット時にhandleSubmitを実行する
<form className="commentForm" onSubmit={this.handleSubmit}> // … 5
<input type="text" placeholder="Your name" ref="author" />
<input type="text" placeholder="Say something..." ref="text" />
<input type="submit" value="Post" />
</form>
);
}
});
// tutorial4.js
var Comment = React.createClass({
render: function() {
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
{this.props.children}
</div>
);
}
});
React.render(
<CommentBox url="/tutorial13.json" pollInterval={2000} />,
document.getElementById('content')
);


まとめ

これでデータ取得と更新ができる様になったのでシンプルな最低限のシステムをReactを使って作ることができます。

自分もまだまだこれからですが「これは使えそう」という印象を強く持ちました。

そして、有用なモノにありがちな初期導入のハードルもさほどでないことが分かりました。

周回遅れ感あふれる感じですが、今年Webを作る時はCakePHP3 + Reactで作ろうと思います!


参考資料

React公式

http://facebook.github.io/react/index.html

Reactチュートリアル

http://facebook.github.io/react/docs/tutorial.html

一人React.js Advent Calendar 2014

http://qiita.com/advent-calendar/2014/reactjs