Edited at

ES5のReact.jsソースをES6ベースに書き換える

More than 1 year has passed since last update.


概要

開発者にとってはメリットがありそうだけどユーザーにとってはいまいちメリットがよく分からない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を使って定義していきます。


単純なパターン


before

var React = require('react');



after

import React from 'react';


これだけ。


モジュール内のメンバをimportする場合


before

var EventEmitter = require('events').EventEmitter;



after

import {EventEmitter} from 'events';


{}で囲みます。別名で扱いたい場合は、asをつけて変数を宣言してあげます。


after

import {EventEmitter as Emitter} from 'events';


同一モジュールから複数のメンバをまとめてimportすることもできます。


before

var ReactRouter = require('react-router'),

Router = ReactRouter.Router,
Route = ReactRouter.Route,
IndexRoute = ReactRouter.IndexRoute,
History = ReactRouter.History,
hashHistory = ReactRouter.hashHistory;


after

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の継承により定義したいと思います。


ES5

var Index = React.createClass({

render: function(){
return (
<div>
{this.props.children}
</div>
);
}
});


ES6

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


アロー関数の導入

以下な感じです。


before

var getUserStoreStates = function(){

return UserStore.getAjaxResult();
};


after

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も改修しました。


before

module.exports = UserBox;



after

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

ES5とES6のソース比較


補足

続:ES5のReact.jsソースをES6ベースに書き換える