React Tutorial Example (Hot Module Replacement)


[React tutorial](https://facebook.github.io/react/docs/tutorial.html) は [React](https://facebook.github.io/react/index.html) の基本を学ぶのに良いテキストです。
React は *HMR(Hot Module Replacement)* がとても便利なので、
ブラウザのリロードの負担を軽減したチュートリアルの進め方を紹介します。

## Learning

* React tutorial を *Hot Module Replacement* を利用する。
* Web server に *webpack-dev-server* を利用する。

## Environment

* node: v4.4.5
* npm: v3.9.6

## Comment Box Form

* 完成される [Source Code](https://github.com/ogom/react-comment-box-example/tree/react) のファイルリストです。

```bash
$ tree -a -I node_modules
.
├── .babelrc
├── app.js
├── index.html
├── index.js
├── package.json
├── style.css
└── webpack.config.js
```

*Let's hands-on*

### Initial application

* `npm init` コマンドで `package.json` ファイルを作成します。

```bash
$ mkdir react-comment-box-example && cd react-comment-box-example
$ npm init --force
```

### Add package

* React で *HMR* を実行するモジュールをインストールします。

```bash
$ npm install webpack webpack-dev-server --save-dev
$ npm install babel-core babel-loader babel-preset-react --save-dev
$ npm install react react-hot-loader react-dom --save-dev
```

* *Markdown parser* は [remarkable](https://github.com/jonschlinkert/remarkable) を使います。
* *Ajax* は [JQuery](https://jquery.com/) を使います。

```bash
$ npm install remarkable jquery --save-dev
```

### Add babelrc

* Babel preset に React plugin を設定します。

```json:.babelrc
{
  "presets": ["react"]
}
```

### Add webpack config

* Webpack に *HMR* の設定をします。
   * `output` の `bundle.js` は物理のファイルは出力されません。

```jacascript:webpack.config.js
var webpack = require('webpack')

module.exports = {
  entry: './index',
  output: {
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ],
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loaders: [
          'react-hot',
          'babel'
        ]
      }
    ]
  },
  devServer: {
    hot: true,
    port: 4000,
    inline: true,
    historyApiFallback: true
  }
}
```

### Add npm start

* `npm start` コマンドで `webpack-dev-server` を実行する設定を `start` にします。

```diff:package.json
   "scripts": {
+    "start": "webpack-dev-server -d --progress --colors",
     "test": "echo \"Error: no test specified\" && exit 1"
   },
```

### Add Stylesheet

* Stylesheet を [React Tutorial](https://github.com/reactjs/react-tutorial) からダウンロードします。

```bash
$ curl https://raw.githubusercontent.com/reactjs/react-tutorial/master/public/css/base.css \
-o style.css
```

### Add View

* HTML に DOM をレンダリングする要素の `<div id='content'>` を設定します。
* `<script src='/bundle.js'>` は Webpack が出力するスクリプトです。

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <title>Comment Box Example</title>
    <link rel='stylesheet' href='/style.css' />
  </head>
  <body>
    <div id='content'></div>
    <script src='/bundle.js'></script>
  </body>
</html>
```

### Add React

* `React` と `ReactDOM` のモジュールのロードを設定します。

```javascript:index.js
var React = require('react');
var ReactDOM = require('react-dom');
```

### Start HTTP Server

* `npm start` コマンドで *Webアプリケーション* を実行します。
* ブラウザで http://localhost:4000 をロードすると **Comment Box Example** が表示されます。

```bash
$ npm start
$ open http://localhost:4000
```

![express](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/node/comment-box/react.png)

[React Developer Tools](https://github.com/facebook/react-devtools)を利用すると要素の確認など便利な機能が使えます。

### Start React Tutorial

ここからが [React tutorial](https://facebook.github.io/react/docs/tutorial.html) です。チュートリアルの順番にコードの差分を表示しています。
(*HMR* を利用するために `index.js` と `app.js` のファイルにコードを分離しています。)

#### Tutorial 1

* `React.createClass` で **CommentBox** コンポーネントを作成します。
* 作成したコンポーネントを `ReactDOM.render` でレンダリングします。

```diff:app.js
+var React = require('react');
+var Remarkable = require('remarkable');
+var $ = require('jquery');
+
+var CommentBox = React.createClass({
+  render: function() {
+    return (
+      <div className="commentBox">
+        Hello, world! I am a CommentBox.
+      </div>
+    );
+  }
+});
+
+module.exports = CommentBox;
```

```diff:index.js
+var CommentBox = require('./app');
+
+ReactDOM.render(
+  <CommentBox />,
+  document.getElementById('content')
+);
```

![express](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/node/comment-box/react_tutorial_1.png)

#### Tutorial 2

* **CommentList** と **CommentForm** のコンポーネントを作成します。

```diff:app.js
+var CommentList = React.createClass({
+  render: function() {
+    return (
+      <div className="commentList">
+        Hello, world! I am a CommentList.
+      </div>
+    );
+  }
+});
+
+var CommentForm = React.createClass({
+  render: function() {
+    return (
+      <div className="commentForm">
+        Hello, world! I am a CommentForm.
+      </div>
+    );
+  }
+});
```

#### Tutorial 3

* **CommentBox** コンポーネントに **CommentList** と **CommentForm** のコンポーネントを追加します。

```diff:app.js
 var CommentBox = React.createClass({
   render: function() {
     return (
       <div className="commentBox">
-        Hello, world! I am a CommentBox.
+        <h1>Comments</h1>
+        <CommentList />
+        <CommentForm />
       </div>
     );
   }
```

![express](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/node/comment-box/react_tutorial_3.png)

#### Tutorial 4

* **Comment** コンポーネントを作成します。
  * `this.props.author` と `this.props.author` はコンポーネントのプロパティがレンダリングされます。

```diff:app.js
+var Comment = React.createClass({
+  render: function() {
+    return (
+      <div className="comment">
+        <h2 className="commentAuthor">
+          {this.props.author}
+        </h2>
+        {this.props.children}
+      </div>
+    );
+  }
+});
```

#### Tutorial 5

* **CommentList** コンポーネントに **Comment** コンポーネントを追加します。
* **Comment** コンポーネントには `author` と *child Nodes* のプロパティを設定します。

```diff:app.js
 var CommentList = React.createClass({
   render: function() {
     return (
       <div className="commentList">
-        Hello, world! I am a CommentList.
+        <Comment author="Pete Hunt">This is one comment</Comment>
+        <Comment author="Jordan Walke">This is *another* comment</Comment>
       </div>
     );
   }
```

![express](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/node/comment-box/react_tutorial_5.png)

#### Tutorial 6

* **Comment** コンポーネントに *Markdown* のレンダリングを追加します。

```diff:app.js
 var Comment = React.createClass({
   render: function() {
+    var md = new Remarkable();
     return (
       <div className="comment">
         <h2 className="commentAuthor">
           {this.props.author}
         </h2>
-        {this.props.children}
+        {md.render(this.props.children.toString())}
       </div>
     );
   }
```

![express](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/node/comment-box/react_tutorial_6.png)

#### Tutorial 7

* `dangerouslySetInnerHTML` で *Markdown* のレンダリングをHTMLの要素に変換します。

```diff:app.js
 var Comment = React.createClass({
-  render: function() {
+  rawMarkup: function() {
     var md = new Remarkable();
+    var rawMarkup = md.render(this.props.children.toString());
+    return { __html: rawMarkup };
+  },
+
+  render: function() {
     return (
       <div className="comment">
         <h2 className="commentAuthor">
           {this.props.author}
         </h2>
-        {md.render(this.props.children.toString())}
+        <span dangerouslySetInnerHTML={this.rawMarkup()} />
       </div>
     );
   }
```

![express](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/node/comment-box/react_tutorial_7.png)

#### Tutorial 8

* **Comment** コンポーネントのプロパティのデータを準備します。
  * データはJSON形式の配列です。

```diff:index.js
+var data = [
+  {id: 1, author: "Pete Hunt", text: "This is one comment"},
+  {id: 2, author: "Jordan Walke", text: "This is *another* comment"}
+];
```

#### Tutorial 9

* まず **CommentBox** コンポーネントのプロパティに `data` を設定します。
* 次に **CommentList** コンポーネントのプロパティに `data` を設定します。

```diff:index.js
 ReactDOM.render(
-  <CommentBox />,
+  <CommentBox data={data} />,
   document.getElementById('content')
 );
```

```diff:app.js
 var CommentBox = React.createClass({
   render: function() {
     return (
       <div className="commentBox">
         <h1>Comments</h1>
-        <CommentList />
+        <CommentList data={this.props.data} />
         <CommentForm />
       </div>
     );
   }
 });
```

#### Tutorial 10

* 配列のデータを **Comment** コンポーネントのプロパティに設定します。
  * `map()` で `commentNodes` の変数にセットされます。

```diff:app.js
 var CommentList = React.createClass({
   render: function() {
+    var commentNodes = this.props.data.map(function(comment) {
+      return (
+        <Comment author={comment.author} key={comment.id}>
+          {comment.text}
+        </Comment>
+      );
+    });
     return (
       <div className="commentList">
-        <Comment author="Pete Hunt">This is one comment</Comment>
-        <Comment author="Jordan Walke">This is *another* comment</Comment>
+        {commentNodes}
       </div>
     );
   }
```

#### Tutorial 11

* データを API で取得するために `url` プロパティにエンドポイントを設定します。

```diff:index.js
 ReactDOM.render(
-  <CommentBox data={data} />,
+  <CommentBox url="http://localhost:3000/api/comments" />,
   document.getElementById('content')
 );
```

(API は [React Tutorial Example (Express)](http://qiita.com/ogomr/items/b90c68764c3c75cf501e) をご利用ください。)

#### Tutorial 12

* `data` をプロパティではなくコンポーネントで値を設定するために `this.state.data` に置き換えます。
  * `getInitialState()` で `data` を初期化します。

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

![express](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/node/comment-box/react_tutorial_12.png)

#### Tutorial 13

* *Ajax* で取得されたデータを `this.setState` で `data` に値を設定します。
  * `componentDidMount()` はコンポーネントが最初にレンダリングされたときにコールされます。

```diff:app.js
 var CommentBox = React.createClass({
   getInitialState: function() {
     return {data: []};
   },
+  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)
+    });
+  },
   render: function() {
     return (
       <div className="commentBox">
```

#### Tutorial 14

* `pollInterval` プロパティを追加して `setInterval()` で定期的にデータを取得します。

```diff:index.js
 ReactDOM.render(
-  <CommentBox url="http://localhost:3000/api/comments" />,
+  <CommentBox url="http://localhost:3000/api/comments" pollInterval={2000} />,
   document.getElementById('content')
 );
```

```diff:app.js
 var CommentBox = React.createClass({
   getInitialState: function() {
     return {data: []};
   },
-  componentDidMount: function() {
+  loadCommentsFromServer: 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)
     });
   },
+  componentDidMount: function() {
+    this.loadCommentsFromServer();
+    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
+  },
   render: function() {
     return (
       <div className="commentBox">
```

#### Tutorial 15

* **CommentForm** コンポーネントにフォームを追加します。

```diff:app.js
 var CommentForm = React.createClass({
   render: function() {
     return (
-      <div className="commentForm">
-        Hello, world! I am a CommentForm.
-      </div>
+      <form className="commentForm">
+        <input type="text" placeholder="Your name" />
+        <input type="text" placeholder="Say something..." />
+        <input type="submit" value="Post" />
+      </form>
     );
   }
 });
```

![express](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/node/comment-box/react_tutorial_15.png)

#### Tutorial 16

* **CommentForm** コンポーネントの `<input>` の `onChange` をハンドルとアタッチします。

```diff:app.js
 var CommentForm = React.createClass({
+  getInitialState: function() {
+    return {author: '', text: ''};
+  },
+  handleAuthorChange: function(e) {
+    this.setState({author: e.target.value});
+  },
+  handleTextChange: function(e) {
+    this.setState({text: e.target.value});
+  },
   render: function() {
     return (
       <form className="commentForm">
-        <input type="text" placeholder="Your name" />
-        <input type="text" placeholder="Say something..." />
+        <input
+          type="text"
+          placeholder="Your name"
+          value={this.state.author}
+          onChange={this.handleAuthorChange}
+        />
+        <input
+          type="text"
+          placeholder="Say something..."
+          value={this.state.text}
+          onChange={this.handleTextChange}
+        />
         <input type="submit" value="Post" />
       </form>
     );
```

#### Tutorial 17

* **CommentForm** コンポーネントの `<form>` の `onSubmit` をハンドルとアタッチします。

```diff:app.js
 var CommentForm = React.createClass({
   handleTextChange: function(e) {
     this.setState({text: e.target.value});
   },
+  handleSubmit: function(e) {
+    e.preventDefault();
+    var author = this.state.author.trim();
+    var text = this.state.text.trim();
+    if (!text || !author) {
+      return;
+    }
+    // TODO: send request to the server
+    this.setState({author: '', text: ''});
+  },
   render: function() {
     return (
-      <form className="commentForm">
+      <form className="commentForm" onSubmit={this.handleSubmit}>
         <input
           type="text"
           placeholder="Your name"
```

#### Tutorial 18

* **CommentForm** コンポーネントの `onCommentSubmit` プロパティをハンドルとアタッチします。

```diff:app.js
 var CommentBox = React.createClass({
   loadCommentsFromServer: 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)
     });
   },
+  handleCommentSubmit: function(comment) {
+    // TODO: submit to the server and refresh the list
+  },
   componentDidMount: function() {
     this.loadCommentsFromServer();
     setInterval(this.loadCommentsFromServer, this.props.pollInterval);
   },
   render: function() {
     return (
       <div className="commentBox">
         <h1>Comments</h1>
         <CommentList data={this.state.data} />
-        <CommentForm />
+        <CommentForm onCommentSubmit={this.handleCommentSubmit} />
       </div>
     );
   }
```

#### Tutorial 19

* **CommentForm** コンポーネントの `this.props.onCommentSubmit` に引数を設定します。

```diff:app.js
 var CommentForm = React.createClass({
   handleSubmit: function(e) {
     e.preventDefault();
     var author = this.state.author.trim();
     var text = this.state.text.trim();
     if (!text || !author) {
       return;
     }
-    // TODO: send request to the server
+    this.props.onCommentSubmit({author: author, text: text});
     this.setState({author: '', text: ''});
   },
   render: function() {
```

#### Tutorial 20

* **CommentBox** コンポーネントの `handleCommentSubmit` に `comment` を追加する *Ajax* を設定します。

```diff:app.js
 var CommentBox = React.createClass({
   handleCommentSubmit: function(comment) {
-    // TODO: submit to the server and refresh the list
+    $.ajax({
+      url: this.props.url,
+      dataType: 'json',
+      type: 'POST',
+      data: comment,
+      success: function(data) {
+        this.setState({data: data});
+      }.bind(this),
+      error: function(xhr, status, err) {
+        console.error(this.props.url, status, err.toString());
+      }.bind(this)
+    });
   },
   componentDidMount: function() {
     this.loadCommentsFromServer();
```

#### Tutorial 21

* `comment.id` に値を設定します。(`id` は **Comment** コンポーネントの `key` に必要です。)
* *Ajax* を実行する前に `setState` を設定します。
  * `comment` がサーバに追加される前に `comment` を表示することができます。

```diff:app.js
 var CommentBox = React.createClass({
   handleCommentSubmit: function(comment) {
+    var comments = this.state.data;
+    // Optimistically set an id on the new comment. It will be replaced by an
+    // id generated by the server. In a production application you would likely
+    // not use Date.now() for this and would have a more robust system in place.
+    comment.id = Date.now();
+    var newComments = comments.concat([comment]);
+    this.setState({data: newComments});
     $.ajax({
       url: this.props.url,
       dataType: 'json',
       type: 'POST',
       data: comment,
       success: function(data) {
         this.setState({data: data});
       }.bind(this),
       error: function(xhr, status, err) {
+        this.setState({data: comments});
         console.error(this.props.url, status, err.toString());
       }.bind(this)
     });
   },
```

![express](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/node/comment-box/react_tutorial_21.png)

*Congrats!*

----

構成部品を **CommentBox**, **CommentList**, **Comment**, **CommentForm** に分離できました。
次は [React tutorial に Redux を追加](http://qiita.com/ogomr/items/64aae7c8d6a113608230) しましょう！