React.js + ES6 + WebPack チュートリアル



`React.js + ES6 + WebPack` のチュートリアルになります。

> 私は普段のプロジェクトでは`AngularJS 1.4` :laughing: を使用しており
> ReactJSを知ったかぶり :relieved: でこれまで周りを**騙して**:sunglasses:きましたが
> そろそろ**本当にヤバイ**感じなので、ちゃんと勉強してみました :stuck_out_tongue: 

#### **元ネタのLearnCode.academyさんのビデオはこちら :arrow_down:**
本記事では説明のため、かなり簡略化してます。

> オリジナルのビデオ
> https://www.youtube.com/watch?v=MhkGQAoc7bc
> オリジナルのソースコード
> https://github.com/learncodeacademy/react-js-tutorials

:tv: 彼の[YouTubeチャンネル](https://www.youtube.com/channel/UCVTlvUkGslCV_h-nSAId8Sw)には、他にも良いビデオがありますので是非ご覧ください。

## [Chap 1 セットアップ](https://www.youtube.com/watch?v=MhkGQAoc7bc)

#### **1. ソースコードの作成**

以下のファイルを作成します。

```bash:新規作成します。
package.json
webpack.config.js
src/index.html
src/js/client.js
```

```json:package.json
{
  "name": "react-tutorials",
  "version": "0.0.0",
  "description": "",
  "main": "webpack.config.js",
  "dependencies": {
    "babel-loader": "^6.2.4",
    "babel-plugin-add-module-exports": "^0.1.2",
    "babel-plugin-react-html-attrs": "^2.0.0",
    "babel-plugin-transform-class-properties": "^6.3.13",
    "babel-plugin-transform-decorators-legacy": "^1.3.4",
    "babel-preset-es2015": "^6.9.0",
    "babel-preset-react": "^6.5.0",
    "babel-preset-stage-0": "^6.5.0",
    "react": "^15.1.0",
    "react-dom": "^15.1.0",
    "webpack": "^1.13.1",
    "webpack-dev-server": "^1.14.1"
  },
  "devDependencies": {},
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}
```

```js:webpack.config.js
var debug = process.env.NODE_ENV !== "production";
var webpack = require('webpack');
var path = require('path');

module.exports = {
    context: path.join(__dirname, "src"),
    devtool: debug ? "inline-sourcemap" : null,
    entry: "./js/client.js",
    module: {
        loaders: [
            {
                test: /\.jsx?$/,
                exclude: /(node_modules|bower_components)/,
                loader: 'babel-loader',
                query: {
                    presets: ['react', 'es2015', 'stage-0'],
                    plugins: ['react-html-attrs', 'transform-class-properties', 'transform-decorators-legacy'],
                }
            }
        ]
    },
    output: {
        path: __dirname + "/src/",
        filename: "client.min.js"
    },
    plugins: debug ? [] : [
        new webpack.optimize.DedupePlugin(),
        new webpack.optimize.OccurenceOrderPlugin(),
        new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }),
    ],
};
```

```html:src/index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>React Tutorials</title>
    <!-- change this up! http://www.bootstrapcdn.com/bootswatch/ -->
    <link href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.6/cosmo/bootstrap.min.css" type="text/css"
          rel="stylesheet"/>
</head>

<body>
    <div id="app"></div>
    <script src="client.min.js"></script>
</body>
</html>
```

```js:src/js/client.js
import React from "react";
import ReactDOM from "react-dom";

class Layout extends React.Component {

    render() {
        return (
            <h1>It works!</h1>
        );
    }
}
const app = document.getElementById('app');

ReactDOM.render(<Layout/>, app);
```

#### **2. セットアップ**

```bash
$ npm install
$ webpack --watch
```

![Screen Shot 2016-06-10 at 10.36.48 PM.png](https://qiita-image-store.s3.amazonaws.com/0/75231/903bea22-4669-d560-8db8-dcd23e473ad3.png)

`src/client.min.js`が生成されます。(このファイルに全てのJSが**パッケージ**されます。少し時間がかかります。)
この時点で下記のフォルダ構成になってるはずです。(`node_modules`除いてます。)

```bash:tree
~/W/myreact1 ❯❯❯ tree -I 'node_modules' .                                                                                     master ✱ ◼
.
├── package.json
├── src
│   ├── client.min.js // WebPackが自動生成します。
│   ├── index.html
│   └── js
│       └── client.js
└── webpack.config.js

2 directories, 5 files
```

#### **3. 実行**

:arrow_forward:ブラウザで `index.html` を開きます。

```bash
$ open index.html
```

`It works!`と表示されればOKです。 :+1: 

適当に`client.js`を書き換えてみましょう。
WebPackが更新を検出して再コンパイルしてくれます。

WebPackのコンパイルが遅いので、`webpack-dev-server`を[使用しましょう。](https://youtu.be/MhkGQAoc7bc?t=501)

package.json を以下のように修正します。

```json:package.json
  "scripts": {
    "dev": "webpack-dev-server --content-base src --inline --hot",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
```

webサーバを起動します。

```bash
$ npm run dev
```

webでアクセスしましょう。
http://localhost:8080/index.html

`client.js`ファイルを書き換えてみましょう。(勝手に更新してくれます。)
さっきよりかなり速くなります :smile: (差分だけ見てコンパイルしてくれるようです。)

## [Chap 2 コンポーネント](https://www.youtube.com/watch?v=fd2Cayhez58)

先ほどの`client.js`の`Layout`コンポーネントをいじってみましょう。

クラス内に**ファンクション**`getVal()`を実装します。
JSXから`{this.getVal()}`で呼び出します。

```js:client.js
class Layout extends React.Component {

    getVal() { // 実装します。
        return "will";
    }

    render() {
        return (
            <h1>It {this.getVal()} works!</h1> // JSX {...}でJavascriptを書きます。ハイライトがおかしい。。。 ><
        );
    }
}
const app = document.getElementById('app');

ReactDOM.render(<Layout/>, app);
```

:arrow_forward: 実行します。`It will works!` と表示されます。

次は、**コンストラクター**`constructor()`を実装して、`this.val`を定義します。

```js:client.js
class Layout extends React.Component {

    constructor() { // コンストラクター
        super();
        this.val = "will"; // ここ
    }

    render() {
        return (
            <h1>It {this.val} works!</h1>
        );
    }
}
const app = document.getElementById('app');

ReactDOM.render(<Layout/>, app);
```

:arrow_forward: 実行します。先ほどと同じ結果になります。

## [Chap 3 複数コンポーネント](https://www.youtube.com/watch?v=vu_rIMPROoQ)

先ほどの`Layout`コンポーネントのコードを別ファイル`components/Layout.js`に切り出します。

`export default`を付けると、他のファイルから**ビジブル**になります。

```js:components/Layout.js
import React from "react";

export default class Layout extends React.Component {

    constructor() {
        super();
        this.val = "will";
    }

    render() {
        return (
            <h1>It {this.val} works!</h1>
        );
    }
}
```

`client.js`を修正します。
インラインの定義を削除して、代わりに`Layout`コンポーネントを**インポート**します。

```js:client.js
import React from "react";
import ReactDOM from "react-dom";
import Layout from "./components/Layout"; // ここ

const app = document.getElementById('app');

ReactDOM.render(<Layout/>, app);
```

:arrow_forward: 実行します。変更前と同じ結果になると思います。

次に新たに`Header`コンポーネントを作ります。
新規に`components/Header.js`を下記のように作成します。

```js:components/Header.js
import React from "react";

export default class Header extends React.Component {

    render() {
        return (
            <h1>I am Header.</h1>
        );
    }
}
```

`Layout.js`を修正し、`Header`コンポーネントをインポートします。
つまり、`Layout`が親コンポーネント、`Header`が子コンポーネントになります。

```js:components/Layout.js
import React from "react";
import Header from "./Header";

export default class Layout extends React.Component {

    render() {
        return (
            <Header/>
        );
    }
}
```

:arrow_forward: 実行します。`I am Header.`と表示されます。

次に、`Header`コンポーネントを３つ、リストに入れて表示してみます。
> :bulb: `return`で返すトップのDOMエレメントは一つである必要があるため`div`で囲まないとエラーになります。 :sweat_drops: 

```js:components/Layout.js
import React from "react";
import Header from "./Header";

export default class Layout extends React.Component {

    render () {
        var list = [
            <Header/>,
            <Header/>,
            <Header/>
        ];

        return (
            <div>
                {list}
            </div>
        );
    }
}
```

:arrow_forward: 実行します。(I am Header.が[３つ表示](https://youtu.be/vu_rIMPROoQ?t=190)されます。)

## [Chap 4 ステートとプロップ](https://www.youtube.com/watch?v=qh3dYM6Keuw)

ステート`state`を使用してみましょう。
ステートは**コンストラクター**で定義します。
その名の通り、コンポーネントの**状態**を保持する、書き換え可能な値です。
`Layout.js`を書き換えます。

```js:Layout.js
import React from "react";

export default class Layout extends React.Component {

    constructor() {
        super();
        this.state = {name1: "Will"}; // ここ
    }

    render () {

        return (
            <div>
                <h1>{this.state.name1}</h1> // 参照します。
            </div>
        );
    }
}
```

:arrow_forward: 実行します。`Will`と表示されます。(どうでもいいですが、ここでは`Will`は人名ですね。 :sweat_drops: )


次に`setTimeout()`を使用して、ステートの値を**変更**してみましょう。

```js:Layout.js
...
   render () {

        setTimeout(() => {
            this.setState({name1: "Bob"}) // １秒後にBobに変更
        }, 1000);

        return (
            <div>
                <h1>{this.state.name1}</h1> // リアルなDOM?もちゃんと更新されます
            </div>
        );
    }
```

1秒後に[WillがBobに変わります。](https://youtu.be/qh3dYM6Keuw?t=219)

次にプロップを使用してみましょう。

`Layout`コンポーネントから、`Header`コンポーネントのプロップ`title1`に**インジェクト**します。

```js:Layout.js
import React from "react";
import Header from "./Header";

export default class Layout extends React.Component {

    render () {

        const title = "Welcome Will!";
        
        return (
            <div>
                <Header title1={title} /> // インジェクト
            </div>
        );
    }
}
```

`Header`コンポーネントで`console.log()`で確認しましょう。

```js:Header.js
import React from "react";

export default class Header extends React.Component {

    render() {

        console.log(this.props); // 出力: Object {title1: "Welcome Will!"}

        return (
            <h1>I am Header.</h1>
        );
    }
}
```

:arrow_forward: 実行します。コンソールに`Object {title1: "Welcome Will!"}`が[出力されますね。](https://youtu.be/qh3dYM6Keuw?t=357)

## [Chap 5 イベント](https://www.youtube.com/watch?v=_D1JGNidMr4)

`Header`コンポーネントに**テキストフィールド**を追加します。

> やりたいことは、**テキストフィールドにタイプした文字列を、リアルタイムに上の部分に表示する** よくあるやつですね。

まずは手始めに仕組みづくりをします。

`Header.js`を以下のように修正します。

```js:Header.js
import React from "react";

export default class Header extends React.Component {

    render() {

        return (
            <div>
                <h1>{this.props.title1}</h1> //テキストフィールドの値を、動的にここに表示したい
                <input />
            </div>               
        );
    }
}
```

`Layout.js`を以下のように修正します。

```js:Layout.js
import React from "react";
import Header from "./Header";

export default class Layout extends React.Component {

    constructor() {
        super();
        this.state = {
            theTitle: "Welcome", // コンポーネントの状態を保たせます。
        };
    }

    render () {

        setTimeout( () => {
            this.setState({theTitle: "Welcome Will!"}); // 3秒後に、状態を変化させます。
        }, 3000);
        
        return (
            <div>
                <Header title1={this.state.theTitle} /> // Headerコンポーネントにインジェクトします。
            </div>
        );
    }
}
```

:arrow_forward: 実行します。３秒後に、表示が変わりましたでしょうか？
もちろん、この段階ではテキストフィールドに値を入力しても何も変わりません。 :worried: 

**完成形**

1. 親の`Layout`コンポーネントに、状態`theTitle`を変更するインターフェース`changeTitleInterface(title)`を実装します。
2. Headerコンポーネントのプロップ`callback1`に、インターフェースを渡します。子コンポーネント`Header`にコールバックさせるわけです。
(bindでthisを親コンポーネントに束縛します。)

```js:Layout.js
import React from "react";
import Header from "./Header";

export default class Layout extends React.Component {

    constructor() {
        super();
        this.state = {
            theTitle: "Welcome",
        };
    }

    changeTitleInterface(title) {
        this.setState({theTitle: title})
    }

    render () {
        return (
            <div>
                <Header title1={this.state.theTitle} callback1={this.changeTitleInterface.bind(this)} />
            </div>
        );
    }
}
```

`Header`コンポーネントを下記のように修正します。

```js:Header.js
import React from "react";

export default class Header extends React.Component {

    handleChange(e) {
        const title = e.target.value;
        this.props.callback1(title); // 親のインターフェースをコールバック
    }

    render() {

        return (
            <div>
                <h1>{this.props.title1}</h1>
                <input onChange={this.handleChange.bind(this)} /> // ココは普通にonChangeイベントをフックします。
            </div>               
        );
    }
}
```

`onChange`イベントをフックして、親のインターフェースをコールバックします。
そのさい、テキストフィールドの値を渡してあげるわけです。

すると、

1. 親の状態`theTitle`が更新される
2. `Header`のプロップ`this.props.title1`も変わる

で、[めでたく表示も変わるわけです。](https://youtu.be/_D1JGNidMr4?t=260)

状態自体はあくまで、親コンポーネントが管理し、子供はステートレスな作りにしておきます。

## [Chap 6 ルーティング](https://www.youtube.com/watch?v=1iAG6h9ff5s)

ルーティングを実装してみましょう。

まず、`react-router`と`history`をインストールします。

```bash
$ npm -S install react-router
$ npm -S install history@1
```

`React Router`を使用して、タブバーを実装してみましょう。
３つのリンクを配置し、クリックしたリンクに応じてページの一部を更新します。

親ページとなる`Layout`コンポーネントと、子ページとなる`Page1`,`Page2`,`Page3`,コンポーネントを作成します。

`client.js`を下記のように修正します。

```js:client.js
import React from "react";
import ReactDOM from "react-dom";
import {Router, Route, IndexRoute, hashHistory} from "react-router"

import Layout from "./components/Layout"; // 親ページ
import Page1 from "./components/Page1"; // 子ページを３つインポート
import Page2 from "./components/Page2";
import Page3 from "./components/Page3";

const app = document.getElementById('app');

ReactDOM.render(
    <Router history={hashHistory}>
        <Route path="/" component={Layout}> // テンプレートとなる親ページ
            <IndexRoute component={Page1}></IndexRoute> // 子ページ１。デフォルトページ
            <Route path="page2" component={Page2}></Route> // 子ページ２
            <Route path="page3" component={Page3}></Route> // 子ページ３
        </Route>
    </Router>,
app);
```


> `import {XXX} from "YYY"` 
> 
> `export default`の**default**が付いていないメンバをインポートする場合は、カーリーで囲みます。

`/page2`にアクセスすると、`Page2`コンポーネントが`Layout`コンポーネントに読み込まれます。

親ページとなる`Layout`コンポーネントを下記のように修正します。
`this.props.children`に、選択した`Page`コンポーネントがセットされます。

```js:components/Layout.js
import React from "react";
import {Link} from "react-router" // リンクを作成するコンポーネント

export default class Layout extends React.Component {

    render () {
        return (
            <div>
                <h1>Layout</h1>

                {this.props.children} // ここに選択した子コンポーネントが読み込まれます。

                <Link to="/">Show Page1</Link> // リンクを３つ作成します。
                <Link to="page2">Show Page2</Link>
                <Link to="page3">Show Page3</Link>
            </div>                
        );
    }
}
```

最後に子ページのファイル３つを作成します。

```js:components/Page1.js
import React from "react";

export default class Page1 extends React.Component {

    render () {
        return (
            <h1>Page1</h1> // 同様にPage2, Page3を作成します。
        );
    }
}
```

:arrow_forward: [実行します](https://youtu.be/1iAG6h9ff5s?t=619)

![Screen Shot 2016-06-11 at 4.52.04 PM.png](https://qiita-image-store.s3.amazonaws.com/0/75231/f219b208-67ba-a5bb-675e-94b284fd5139.png)

URLを見ると、SPAの特徴である**ハッシュ**`#`でルーティングしているのがわかります。

[Flux編へ](http://qiita.com/tfutada/items/d068a0fda78457280778)