console.blog(self);

技術、読んだ本、いろいろ。

React TutorialをES6で書きなおしてみた

できあがったのはこちら。

Reactを触ってみたいと思って、チュートリアルをやってみた。翻訳してくださっているかたがいて、訳もわかりやすくてとても助かった。

チュートリアルでは JSXTransformer.js を使っているので、JSXの変換などを考える必要はない。手軽なんだけど、Reactを使おうとしたらけっきょくコンパイルしなきゃいけない。

コンパイルにはBabelが必要になる。Babel使うならES6も使いたい。ES6の変換は自動的にやりたいし、ファイルはminifyしたいし、minifyしたらsource mapが欲しいし、変更したら自動的にブラウザを更新したい。って感じであれこれやってみた。

Environment

ディレクトリ構成はこんな感じ。

react-tutorial-es6
├── LICENSE
├── README.md
├── comments.json
├── dist
│   ├── app.js
│   └── app.js.map
├── gulpfile.js
├── index.html
├── package.json
├── public
│   └── index.html
├── server.js
└── src
    └── app.jsx

package.jsonはこんな感じ。

gulpfile.jsはこんな感じ。

var gulp        = require('gulp');
var browserify  = require('browserify');
var babelify    = require('babelify');
var uglify      = require('gulp-uglify');
var source      = require('vinyl-source-stream');
var buffer      = require('vinyl-buffer');
var sourcemaps  = require('gulp-sourcemaps');
var filter      = require('gulp-filter');
var nodemon     = require('gulp-nodemon');
var browserSync = require('browser-sync').create();

gulp.task('browserify', function() {
  browserify('./src/app.jsx', { debug: true })
    .transform(babelify)
    .bundle()
    .on("error", function (err) { console.log("Error : " + err.message); })
    .pipe(source('app.js'))
    .pipe(buffer())
    .pipe(sourcemaps.init({loadMaps: true}))
    .pipe(uglify())
    .pipe(sourcemaps.write('./'))
    .pipe(gulp.dest('./dist'))
});

gulp.task('watch', function() {
  gulp.watch('./src/*.jsx', ['browserify'])
});

gulp.task('browser-sync', ['nodemon'], function() {
  browserSync.init(null, {
      proxy: 'http://localhost:3000',
      port: 8000
  });
  gulp.watch(["public/**", "dist/**"], function() {
    browserSync.reload();
  });
});

gulp.task('nodemon', function() {
  return nodemon({
    script: 'server.js'
  }).on('restart', function() {
    setTimeout(function() {
      browserSync.reload();
    }, 500);
  });
});

gulp.task('default', ['browserify', 'watch', 'browser-sync']);

browserifyでES6を変換したり、source map作ったり、minifyしたりする。これだけでもいろいろはまった。

チュートリアルではサーバが必要になる。いろいろな言語でのサーバのサンプルが付いている。Node.jsのサーバ用のスクリプトをnodemonで起動する。サーバ起動後にbrowser-syncを動かす。ここでもいろいろはまった。

ふぃー。

ES6

JavaScriptをES6に書きなおしていく。

import

必要なライブラリの読み込みはimportで行う。bowerは使わないことにした。

これを

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello React</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
  </head>
  <body>
    <div id="content"></div>
    <script type="text/jsx">
      // Your code here
    </script>
  </body>
</html>

こんな感じにapp.jsにまとめる。

<DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Hello React</title>
</head>
<body>
  <div id="content"></div>
  <script src="./dist/app.js"></script>
</body>
</html>

importはこんな感じ。JSXTransformer.jsはコンパイルするからいらなくなる。markedはあとで使う。

import React  from 'react';
import $      from 'jquery';
import marked from 'marked';

React.Component

React.createClassはReact.Componentを継承したクラスとして書きなおす。

これを

// tutorial1.js
var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        Hello, world! I am a CommentBox.
      </div>
    );
  }
});
React.render(
  <CommentBox />,
  document.getElementById('content')
);

こんな感じにする。他の部分も同様に書きなおす。

// tutorial1.js in ES6
class CommentBox extends React.Component {
  render() {
    return (
      <div className="commentBox">
        Hello, world! I am a CommentBox.
      </div>
    );
  }
}
React.render(
  <CommentBox />,
  document.getElementById('content')
);

let

varはletにする。これを

    var rawMarkup = marked(this.props.children.toString(), {sanitize: true});

こんな感じにする。他の部分も同様に書きなおす。

    let rawMarkup = marked(this.props.children.toString(), {sanitize: true});

getInitialState

getInitialStateはコンストラクタに書きなおす。これを

// tutorial12.js
var CommentBox = React.createClass({
  getInitialState: function() {
    return {data: []};
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

こんな感じにする。

class CommentBox extends React.Component {
  constructor(props) {
    super(props);
    this.state = {data: []};
  }
  render() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
}

thisのあれこれ

thisが迷子になった。

Ajaxを書きなおす

コールバックをArrow Functionにする。bind(this)が不要になる。これを

// tutorial13.js
var CommentBox = React.createClass({
  // ...

  componentDidMount: 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)
    });
  },

  // ...
});

こんな感じにする。他の部分も同様に書きなおす。

class CommentBox extends React.Component {
  // ...

  componentDidMount() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      cache: false,
      type: 'POST',
      success: data => this.setState({data: data}),
      error: (xhr, status, err) => console.error(this.props.url, status, err.toString())
    });
  }

  // ...
}

setInterval

こちらもthisが変わってしまう。これを

// tutorial14.js
var CommentBox = React.createClass({
  // ...

  componentDidMount: function() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
  },

  // ...
});

こんな感じに書きなおす。

class CommentBox extends React.Component {
  // ...

  componentDidMount() {
    this.loadCommentsFromServer();
    setInterval( () => this.loadCommentsFromServer(), this.props.pollInterval );
  }
  // ...
}

this.loadCommentsFromServer だとthisが変わって loadCommentsFromServerthis.props.url が見つからなくなるので

    setInterval( this.loadCommentsFromServer, this.props.pollInterval );

こんな感じになった。

    setInterval( () => this.loadCommentsFromServer(), this.props.pollInterval );

こんな感じでもよい。

    setInterval( this.loadCommentsFromServer.bind(this), this.props.pollInterval );

onSubmit

こちらもthisが変わってしまうので、onSubmit={this.handleSubmit}

// tutorial16.js
var CommentForm = React.createClass({
  handleSubmit: function(e) {
    // ....
  },
  render: function() {
    return (
      <form className="commentForm" onSubmit={this.handleSubmit}>
        <input type="text" placeholder="Your name" ref="author" />
        <input type="text" placeholder="Say something..." ref="text" />
        <input type="submit" value="Post" />
      </form>
    );
  }
});

onSubmit={this.handleSubmit.bind(this)} に書きなおす。

class CommentForm extends React.Component {
  handleSubmit(e) {
    // ....
  }

  render() {
    return (
      <form className="commentForm" onSubmit={this.handleSubmit.bind(this)}>
        <input type="text" placeholder="Your name" ref="author" />
        <input type="text" placeholder="Say something..." ref="text" />
        <input type="submit" value="Post" />
      </form>
    );
  }
}

onCommentSubmit

onCommentSubmitのthisも変わってしまうので onCommentSubmit={this.handleCommentSubmit}

// tutorial17.js
var CommentBox = React.createClass({
  // ...

  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm onCommentSubmit={this.handleCommentSubmit} />
      </div>
    );
  }
});

onCommentSubmit={this.handleCommentSubmit.bind(this)} に書きなおす。

class CommentBox extends React.Component {
  // ...
  render() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data}/>
        <CommentForm onCommentSubmit={this.handleCommentSubmit.bind(this)} />
      </div>
    );
  }
}

app.jsx

最終的にはこんなコードになった。

"use strict";

import React  from 'react';
import $      from 'jquery';
import marked from 'marked';

class CommentBox extends React.Component {
  constructor(props) {
    super(props);
    this.state = {data: []};
  }

  loadCommentsFromServer() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      cache: false,
      success: data => this.setState({data: data}),
      error: (xhr, status, err) => console.error(this.props.url, status, err.toString())
    });
  }

  handleCommentSubmit(comment) {
    let comments = this.state.data;
    let newComments = comments.concat([comment]);
    this.setState({data: newComments});
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      type: 'POST',
      data: comment,
      success: data => this.setState({data: data}),
      error: (xhr, status, err) => console.error(this.props.url, status, err.toString())
    });
  }

  componentDidMount() {
    this.loadCommentsFromServer();
    setInterval( this.loadCommentsFromServer.bind(this), this.props.pollInterval );
  }

  render() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data}/>
        <CommentForm onCommentSubmit={this.handleCommentSubmit.bind(this)} />
      </div>
    );
  }
}

class CommentList extends React.Component {
  render() {
    let commentNodes = this.props.data.map( comment => {
      return (
        <Comment author={comment.author}>
          {comment.text}
        </Comment>
      );
    });
    return (
      <div className="commentList">
        {commentNodes}
      </div>
    );
  }
}

class CommentForm extends React.Component {
  handleSubmit(e) {
    e.preventDefault();
    let author = React.findDOMNode(this.refs.author).value.trim();
    let text   = React.findDOMNode(this.refs.text).value.trim();
    if (!text || !author) {
      return;
    }
    this.props.onCommentSubmit({author: author, text: text});
    React.findDOMNode(this.refs.author).value = '';
    React.findDOMNode(this.refs.text).value   = '';
    return;
  }

  render() {
    return (
      <form className="commentForm" onSubmit={this.handleSubmit.bind(this)}>
        <input type="text" placeholder="Your name" ref="author" />
        <input type="text" placeholder="Say something..." ref="text" />
        <input type="submit" value="Post" />
      </form>
    );
  }
}

class Comment extends React.Component {
  render() {
    let rawMarkup = marked(this.props.children.toString(), {sanitize: true});
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        <span dangerouslySetInnerHTML={{__html: rawMarkup}} />
      </div>
    );
  }
}

React.render(
  <CommentBox url="comments.json" pollInterval={2000} />,
  document.getElementById('content')
);

ふぃー。

まとめ

完成したチュートリアルをがっと書きなおしてみたら、いろいろ動かなくて、けっきょく step by step で書きなおした。ES6力が足りなかった…。

実際に手を動かして、たくさんはまって、悩んで、解決していくという過程は大切だと思った。Arrow Functionのthisの動作は知っていたけど、書いてみたらはまった。やっぱり知っていることとできることは違うよね。

Reactさわってみたかったし、gulpもそのうち使ってみたかったし、ES6も試してみたかったから、まあいいんだけど、予備知識なくはじめるのは大変だなぁと思った。とはいえ情報はたくさんあるし(古い情報も多いけど)、2日くらい触れば簡単なことはできるようになる。学習コストよりはメリットのほうが大きい。当然だけど。

前々から思っていたけど、最近のフロントエンドの環境構築はサーバサイドのそれに似ている。Railsの環境構築みたいな。でもいまのフロントエンドの環境構築はいろいろ過渡期で大変だと思う。Railsの環境構築も同じような時期があったけど、ある程度落ち着いてきた気がする。

ちょうどいい粒度の内容で、書いていて楽しかった。やっぱりプログラミングはいいな。

参考URL