概要
開発者にとってはメリットがありそうだけどユーザーにとってはいまいちメリットがよく分からないES6。ひとまずどのくらい開発者にとってメリットがあるのかを実際に把握するため、ES5のReact.jsソースをES6ベースに書き換えてみました。
対象のソースは、投稿[react-router v2.xとcontext]のサンプルソース(https://github.com/kunitak/react-router-1to2)です。
参考
React TutorialをES6で書きなおしてみた - console.blog(self);
http://sadah.hatenablog.com/entry/2015/08/03/085828
Babelで理解するEcmaScript6の import / export
http://qiita.com/inuscript/items/41168a50904242005271
本家サイト
https://facebook.github.io/react/docs/reusable-components.html#es6-classes
ES6ベースに書き換えてみる
Babelのインストール
babelifyと必要なプリセットをインストールしました。
$ npm install --save babelify babel-preset-es2015 babel-preset-react
ソースの改修
client/scripts以下のソースをES6ベースに改修していきます。
importの改修
requireを使ってモジュールをimportしていた箇所をimportを使って定義していきます。
単純なパターン
var React = require('react');
import React from 'react';
これだけ。
モジュール内のメンバをimportする場合
var EventEmitter = require('events').EventEmitter;
import {EventEmitter} from 'events';
{}で囲みます。別名で扱いたい場合は、asをつけて変数を宣言してあげます。
import {EventEmitter as Emitter} from 'events';
同一モジュールから複数のメンバをまとめてimportすることもできます。
var ReactRouter = require('react-router'),
Router = ReactRouter.Router,
Route = ReactRouter.Route,
IndexRoute = ReactRouter.IndexRoute,
History = ReactRouter.History,
hashHistory = ReactRouter.hashHistory;
import {Router, Route, IndexRoute, History, hashHistory} from 'react-router';
コンポーネント定義の改修
ES5ではコンポーネント定義はcreateClassを使って定義していました。
しかし、公式ページ
https://facebook.github.io/react/blog/2015/03/10/react-v0.13.html
では、
Our eventual goal is for ES6 classes to replace React.createClass completely,
(我々の最終目標はES6のクラスからReact.createClassを完全になくすことだ)
と述べており、本家サイト(es6-classes)が提示しているように、ES6ではcreateClassではなく、Componentの継承により定義したいと思います。
var Index = React.createClass({
render: function(){
return (
<div>
{this.props.children}
</div>
);
}
});
class Index extends React.Component{
render(){
return (
<div>
{this.props.children}
</div>
);
}
}
classとして、React.Componentを継承して定義しています。
そのため、extends使って次のようなこともできます。
//ボディの定義
class Body extends React.Component{
constructor(props) {
super(props);
this.state = {message: ''}; //getInitialStateの代わりにconstructorで設定する
}
render(){
return (
<h1>ポータル {this.state.message}</h1>
);
}
}
//継承:ボディの定義
class ParentBody extends Body{
componentDidMount() {
this.setState({message: 'huga'});//画面では[ポータル huga]と表示される
}
}
さて、createClassでは定義したことのない、constructorというメソッドが出てきました。
"React.createClass" vs "extends React.Component"
React.Componentを使用した場合、幾つかcreateClassとは異なる仕様にぶち当たります。
以下のような違いがありました。
React.createClass | React.Component | |
---|---|---|
thisのオートバインディング | ○ 全てのメソッドにthisをバインドしてくれる |
× constructorでthisをバインドするように記述する必要がある |
mixins利用可否 | 可 | 不可 |
state初期化 | getInitialStateメソッドを使用して初期化 | constructorでobjectの直接代入により初期化(getInitialStateメソッドは存在しない) |
props初期値指定 | getDefaultPropsメソッドを使用して指定 | コンポーネント外部でdefaultPropsオブジェクトに代入する(getDefaultPropsメソッドは存在しない) |
protoTypes、contextTypes定義 | コンポーネント内で定義 | コンポーネント外部でprotoTypes、contextTypesオブジェクトに代入する |
React.createClassはコンポーネントのインスタンス時に色々やってくれていたということがわかりました。もちろんその時にReact.Componentも取り込んでいました。
反対の視点で言えば、実装者にとって、いらないこともやっている、という見方もできます。
React.Componentのみでの宣言は、そういったある意味余計な処理は一切利用せず、実装者が都合に合わせて記述できる、というメリットがあると思います(後で調べたら本家サイトで「開発者は柔軟にコンポーネント実装ができるようになる」というようなことを述べていました)。
ちなみにmixinsについては、今後もES6での対応は予定していないとのこと。
https://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html#mixins
では、引き続きReact.Componentで実装していきます。
constructorの実装
superクラスのconstructor実装
constructorの実装は必須ではないのですが、何らかの理由で自分で実装する場合は、superクラスのconstructorも実装してあげる必要があります。
constructor(props) {
super(props);
//anything to do
}
contextを利用する場合
contextを利用していて、constructorで何かしたい場合は、以下のように書けば良いようです。
constructor(props, context) {
super(props, context);
//anything to do
}
state初期化
getInitialStateメソッドがないので、this.stateに直接オブジェクトを代入します。
constructor(props) {
super(props);
this.state = {message: ''}; //getInitialStateの代わりにconstructorで設定する
}
componentDidMount() {
this.setState({message: 'huga'});
}
render(){
return (
<h1>ポータル {this.state.message}</h1>
);
}
カスタムメソッドへのthisのバインド
stateやprops、refsなどを扱うメソッドを自分で実装する場合は、thisをそのメソッドにバインドしてあげる必要があります。バインドしないとthisは当然nullのためエラーになります。
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);//バインド
this.state = {message: ''};
}
handleSubmit(){
var name = ReactDOM.findDOMNode(this.refs.name).value.trim();
var mail = ReactDOM.findDOMNode(this.refs.mail).value.trim();
this.props.addUser(name, mail);
}
componentDidMount() {
this.setState({message: 'huga'});
}
props初期値指定、protoTypes、contextTypes定義
これらはコンポーネント定義後に外部で実装します。
props初期値指定、protoTypes定義
class User extends React.Component{
render(){
return (
<tr>
<td>{this.props.name}</td>
<td>{this.props.mail}</td>
</tr>
);
}
}
//propTypesは外で定義する
User.propTypes = {
name: React.PropTypes.string.isRequired,
mail: React.PropTypes.string
};
//props初期値指定
User.defaultProps = {name: "hoge"};
contextTypes定義
class Header extends React.Component{
render(){
return (
<header>
<Link to="/portal" style={{paddingRight: "5px"}}>ポータル</Link>
</header>
);
}
};
//contextTypesは外で定義する
Header.contextTypes = {
router: React.PropTypes.object.isRequired
}
varをlet、constに改修する
ES6からlet、constで変数宣言できるようになっているのでvarを改修しました。
ちなみに違いは以下のとおりです。
- let
- 同一スコープでの再定義が不可(varは可能)。
- const
- 再代入も再定義も不可。
詳細を知りたい方は以下でどうぞ。
http://solutionware.jp/blog/2016/01/08/es6の新機能を学ぶ-constキーワードとletキーワード/
一回代入したらそのまま使って終わりな変数が大部分なので、だいたいconstになってしまいそうです。letはfor文のiとかで使うケースが多いのかな。
handleSubmit(){
const name = ReactDOM.findDOMNode(this.refs.name).value.trim();
const mail = ReactDOM.findDOMNode(this.refs.mail).value.trim();
this.props.addUser(name, mail);
}
アロー関数の導入
以下な感じです。
var getUserStoreStates = function(){
return UserStore.getAjaxResult();
};
const getUserStoreStates = () => UserStore.getAjaxResult();
returnを書かなくても値を返してくれます。
複数行にわたる場合は、{}で囲んであげます。
const ajax = {
get : (url, params, callback) => {
request
.get(url)
.query(params)
.end((err, res) => callback(err, res))
},
post : (url, params, callback) => {
request
.post(url)
.send(params)
.end((err, res) => callback(err, res))
}
};
exportの改修
ついでにexportも改修しました。
module.exports = UserBox;
export default UserBox;
まとめ
書き換え作業を通じて思ったメリット・デメリットは以下のとおりでした。
メリット
- スキルトランスファーしやすい:
- 例:javaエンジニアでも理解しやすい構造になってきている。importとかclass、extendsとか知ってる単語が出てくる。
- extendsで簡単に既存のクラスが継承できるので、開発効率が上がる。
- アロー関数により、thisをthatに代入して、とか素人目で意味不明なことをしなくて済む。
デメリット
- es6で書いて、babelかますと、es5で書くよりサイズがでかくなってしまう。
- es5で解釈できるようにするために、babelify時に無駄にメソッドが増えてしまうため。
- また、実装時のコードはbabelifyにより若干改変されることを頭に入れてデバッグする必要がある。(許容できるレベルではある)
- ソースの管理コストを考えると、当然gulpタスクもnode.jsもes6ベースで書くことを考えねばならない。
- 他のプロジェクトは、、まあいいか。
来る日に備えて手始めに社内ツールから始めるというのも手かなあと感じました。
サンプルソース
https://github.com/kunitak/es5-to-es6