2015.02.10
既存の Javascript のコードを React で書き換えてみる
フロントエンド系に興味があり技術調査を担当している D.M. です。今回は Facebook 謹製の Javascript ライブラリである React を利用して既存の Javascript のコードの書き換えに挑戦したいと思います。
[これまでのJSネタ]
・Web Components で共通ヘッダーをつくってみた
・ECMAScript 6 で Backbone.js を利用してみる
・既存の Javascript のコードを ECMAScript 6 で書き換えてみる
React とは
Facebook が開発した Javascript の画面描画のためのライブラリです。特徴は以下の3点です。
JUST THE UI
MVC の View 部分をコンポーネント化して操作する機能だけに特化しています。これにより他のライブラリ・フレームワークと組み合わせて利用できるメリットがあります。たとえば Facebook の公開しているサンプルコードでは ajax の処理に jQuery を利用しています。また Javascript の MVC ・ MVVM フレームワークが持っている機能と組み合わせることも可能です。
VIRTUAL DOM
対象となる描画部分について、本体の DOM とは別に、仮想の DOM でツリーを構築します。仮想 DOM の構築は状態( state )が変更されるタイミングで行われ、前回の仮想 DOM との差分を本体 DOM に反映します。この差分反映アルゴリズムのメリットは、処理が Javascript 内で完結するため非常に軽く高速に実行することができる点です。
DATA FLOW
View の描画の処理の流れは常に一方向です。描画開始イベントがシンプルになるためコードがわかりやすくなるメリットがあります。
詳細は本家サイトを参照してください。
React
http://facebook.github.io/react/
2014年の段階で利用実績は Facebook, Instagram をはじめ、 Github, Yahoo など大手サイトへと広がっています。
今回の実装で利用した機能
JSX( Javascript XML )
これはXMLライクな記法です。HTMLを独自に拡張していくようなイメージで利用でき、いわゆるテンプレートエンジンの役割を果たします。
この記法は必須ではありません。従来の Javascript の実装でも動作可能です。ただこの記法を利用したほうがシンプルになります。
// Underscore.js のテンプレートの場合は String var Hello = _.template('<p><%= name %>さんこんにちは</p>'); // JSX を利用すると String にせずに直接記述する。変数は {} で埋め込む。これで <Hello /> タグを生成。 var Hello = React.createElement( React.createClass({ render: function() { <div>{this.props.name}さんこんにちは</div> ); } }));
独自コンポーネントタグと属性 props と状態 state
React の JSX 内では独自コンポーネントタグを生成できます。これにより処理ごとにコンポーネントを分けることができ、可読性が向上します。またコンポーネントはタグの属性値として受けとる props と、内部の処理によって変更される状態 state を持ちます。state が変更されると HTML 描画の render ファンクションが自動的に呼ばれます。その前後にもイベントが発生しますので、事前事後の処理を分けて書くことができます。
<script type="test/jsx"> //Helloコンポーネントを生成する。大文字で定義する。 var Hello = React.createClass({ //初期値。 getInitialState : function(){ return {message:"こんにちは"} }, //ボタンを押したときの処理でstateを変える。 changeMsgByOnClick : function(){ this.setState({message:"さようなら"}); }, render : function(){ //this.propsでタグの属性値が取得できる。 return ( //レスポンスは必ず1つのタグになる。全体をひとつのタグで囲む必要がある。 <div> <span> {this.props.name}さん{this.state.message}。 </span> <button onClick={this.changeMsgByOnClick} /> </div> ); } }); React.renderComponent( <Hello>, //id=HelloをHelloコンポーネントで置き換える。 document.getElementById('Hello') ); </script> <div =id="Hello" name="Pepper"></div> //表示は「Pepperさんこんにちは」になります。
既存コードを書き換えてみる
React を試験的に書いてみます。このブログでは何度か取り扱っている 9199.jp の「駅の周辺検索」を題材にします。具体的には県名選択⇒路線セレクトボックスの更新の処理が React で動作するようになっています。
処理の流れ
1.初期処理
以下の順でコンポーネントが描画されます。
SelectPrefLineBox
SelectPref
Pref
SelectLine
Line
2.県選択時の更新処理
SelectPref コンポーネントの onChange イベントで選択された pref_code を SelectLine のプロパティとして設定すると再度描画が行われビューが更新されます。コンポーネントタグのプロパティは props として扱われ、コンポーネント内部で処理結果を state として持ちます。 props または state の変更のタイミングでイベントが発生するので、内部で ajax による路線の値取得を行わせています。
実際のコード
検証ページ
http://9199.jp/js-test/react-test.html
// React の本体JS <script src="/common/js/react/react-0.12.2/react.min.js"></script> // JSX を読み込ませるための JS ライブラリ。サーバサイドでのプリコンパイルも可能。 <script src="/common/js/react/react-0.12.2/JSXTransformer.js"></script> //JSX 宣言をします。 <script type="text/jsx"> //県の値のコンポーネントを生成する。 var Pref = React.createClass({ //県の値のHTMLを描画する。 render: function() { return ( <option value={this.props.pref_code}>{this.props.pref_name}</option> ); } }); //県のセレクトボックスのコンポーネントを生成する。 var SelectPref = React.createClass({ //初期値を設定する。 getInitialState : function() { return {data: [ {"pref_code":"0","pref_name":"都道府県" },{ "pref_code":"1","pref_name":"北海道"},{"pref_code":"2","pref_name":"青森県"} ] }; }, //セレクトボックス選択時に発火する。 setLine : function(){ console.log("setLine"); //親のBoxクラスに発火時の選択駅の値を伝える。 this.props.onPrefChange(this.refs.selectP.getDOMNode().value); }, //HTMLを描画する。 render : function() { console.log("renderPref"); //現在のstateに入っている県の値からセレクトボックスの中身を生成 var prefs = this.state.data.map(function (p) { return <Pref pref_code={p.pref_code} pref_name={p.pref_name} /> ; }); //セレクトボックスとして描画する。 return ( <select name="stationpref" id="stationpref" class="l" onChange={this.setLine} ref="selectP"> {prefs} </select> ); } }); //路線の値のコンポーネント var Line = React.createClass({ render: function() { console.log( "render in Line" ); return ( <option value={this.props.line_code}>{this.props.line_name}</option> ); } }); //路線のセレクトボックスのコンポーネント var SelectLine = React.createClass({ //初期値 getInitialState : function() { return {lines: [], lineComp:[]}; }, //コンポーネントが生成されたときに呼ばれる。 componentDidMount : function(){ console.log(" componentDidMount"); //路線セレクトボックスの更新処理 this.reloadLine(this.props.selectPrefId); }, //コンポーネントが新しいプロパティを受け取った場合に呼ばれる。 componentWillReceiveProps : function(nextProps) { console.log(" componentWillReceiveProps "); //路線セレクトボックスの更新処理 this.reloadLine(nextProps.selectPrefId); }, //路線のHTMLの描画 reloadLine: function(pref) { console.log(" reloadLine"); var newLines = [ {"line_code":"0" , "line_name":"路線を選択" } ]; self = this ; //未選択時はゼロなので初期処理をする。 if(pref==0){ this.state.lines = newLines; console.log("this.state.lines"); console.log(this.state.lines); var lines = this.state.lines.map(function (l) { return <Line line_code={l.line_code} line_name={l.line_name} /> ; }); self.setState({lineCmpt : lines }); }else{ //選択された県がある場合は ajax でサーバからデータを取得する。 console.log("ajax"); var requestUrl = ""; var ua = jQuery.browser; if(ua.mozilla) requestUrl = '/common/json/station/line_mozilla/' + pref + '.json'; else requestUrl = '/common/json/station/line_ie/' + pref + '.json'; //芸も無く jQuery の ajax 処理だがシンプル。 jQuery.getJSON(requestUrl, function(json) { jQuery.each(json.line, function(i,l){ if( l ){ newLines.push( { "line_name":l.line_name, "line_code":l.line_cd } ); } }); self.state.lines = newLines; console.log("this.state.lines"); var lines = self.state.lines.map(function (l) { return <Line line_code={l.line_code} line_name={l.line_name} /> ; }); //この setState の瞬間に render ファクションにて再描画 self.setState({lineCmpt : lines }); console.log("finish ajax"); }); } }, //駅の描画処理(ここは既存のJSを呼んでいるのでReact化されていない) setStation : function(){ setStationCode(this.refs.selectL.getDOMNode().value); }, //路線コンポーネントのHTML描画。Ajaxの処理後に呼ばれる。 render : function() { console.log("renderLine"); lines = this.state.lineCmpt; return ( <select name="linecode" id="linecode" class="l" onChange={this.setStation} ref="selectL"> {lines} </select> ); } }); //県・路線コンポーネントを生成。処理全体をまとめる。 var SelectPrefLineBox = React.createClass({ //初期値。県の選択なしの状態を設定。 getInitialState : function() { return {selectPrefId: 0}; }, //県を選択時にstateを更新する。これによりSelectLineコンポーネントのpropsが変わりrenderが発火する。 lineReload : function(pref) { console.log("Box lineReload"); this.setState({selectPrefId: pref}); }, //県・路線セレクトボックスを描画する。 render : function() { return ( <ul> <li><SelectPref onPrefChange={this.lineReload} /></li> <li><SelectLine selectPrefId={this.state.selectPrefId} /></li> <li> <select name="stationcd" id="stationcode" class="l"> <option value="0">駅名を選択 </option> </select> </li> <li><input type="submit" class="button l" value="検索" /></li> </ul> ); } }); // SelectPrefLineBox コンポーネントを id=SelectPrefLineBox に設定して描画する。 React.render( <SelectPrefLineBox/>, document.getElementById('SelectPrefLineBox') ); </script> <div id="SelectPrefLineBox"></div>
所感
・とても書きやすい印象。JSX について記述の自由度がとても好みです。これが好きになれるかどうかが評価の分かれ目ではないでしょうか。
・今回の例ではデータフローアーキテクチャの Flux を利用できていないので、もう少し精進する必要あり。
今後の展望
View に特化しているため JS フレームワークと連携については個別の最適化は追求できる要素だと思います。 React のコンポーネントの排他性を強めると CSS ・ style の扱いをコンポーネント内に保持する必要が出てくるため、その手法についても検証の余地があります。
また、現状 UI フレームワークが乱立しているので、利点を比較し見極めが必要になってきています。また Web Components ・ Polymer との立ち位置の違いについても整理したいと思います。
グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。
Follow @GMO_RD