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

  • 110
    Like
  • 0
    Comment

概要

開発者にとってはメリットがありそうだけどユーザーにとってはいまいちメリットがよく分からない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ベースに書き換える