React + ES6 + Webpack で JSON Visual Editor を作ってみる

# はじめに

ES6 や React、Webpack といった技術の勉強のために簡単なシンプルページアプリケーションを作って、その過程をここに記録として書いていきます。時系列に何の整理もせず書いていくので分かりづらいかもしれないですが、[ソースコードも公開](https://github.com/ogaoga/json-visual-editor)するので、何かの参考になれば幸いです。

# 作るもの

JSON を見やすく可視化して、かつ編集も可能な [JSON Visual Editor](https://ogaoga.github.io/json-visual-editor/) を作ります。これの元となるのはずいぶん前に作った「[JavaScript のオブジェクトを可視化するやーつ](http://jsdo.it/ogaoga/mlE0)」で、もっと使いやすくしたかったのと、React の勉強にちょうど良さそうなので、作ってみることにしました。

* 動作する実物：[JSON Visual Editor](https://ogaoga.github.io/json-visual-editor/)
* ソースコード：[ogaoga/json-visual-editor](https://github.com/ogaoga/json-visual-editor)

![json-visual-editor.png](https://qiita-image-store.s3.amazonaws.com/0/4617/b1011c1c-62c4-5a38-d17c-42708f7b8296.png)

# 環境構築

今回は、React を ES6 (Babel) で、スタイルは Stylus で書いて、Webpack でビルドします。Gulp などは使用せず、npm-scripts でタスクを実行します。

これを１から構築するのではなく、これに似た構成のプロジェクトをベースに構築しました。

* [alicoding/react-webpack-babel: Simple React Webpack Babel Starter Kit](https://github.com/alicoding/react-webpack-babel)

これをフォークしたのがこちらです。

* [ogaoga/json-visual-editor](https://github.com/ogaoga/json-visual-editor)

この JSON Visual Editor は、このリポジトリでオープンソースとして公開します。

## ビルド

[README](https://github.com/alicoding/react-webpack-babel/blob/master/README.md) に記載の通りにビルドします。

## Bootstrap の削除

```
$ npm run dev
```

でビルドしたら、Bootstrap に関係するエラーが出たので、[Bootstrap を削除しました](https://github.com/ogaoga/json-visual-editor/commit/d8b25c1e912d7a3d9abe87404e0a7fa138f54be6)。

## src ディレクトリ追加

`src/` ディレクトリを作ってそこにソースを入れるようにします。試しに Page コンポーネントを作って、`src/index.jsx` から読み込んでみます。

```js:src/index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import Page from './Page';

export class App extends React.Component {
  render() {
    return (
      <Page />
    );
  }
}

ReactDOM.render(<App/>, document.querySelector("#myApp"));
```
```js:src/Page.jsx
import React from 'react';
import ReactDOM from 'react-dom';

export default class Page extends React.Component {
  render() {
    return (
      <div>Page</div>
    );
  }
}
```

`Page` クラスに `default` キーワードをつけず、エラーとなってしまいハマりました。

また、[webpack の設定も変更](https://github.com/ogaoga/json-visual-editor/commit/33370173ec27569bc1a81abdcd33e8a7d4d75449)しました。

## Stylus の設定

スタイルシートの言語は Stylus が好きなので、その設定を行います。[stylus-loader](https://github.com/shama/stylus-loader) という Webpack のプラグインがあるので、[README](https://github.com/shama/stylus-loader#install)に従ってインストール。

現在の構成では、プラグインの指定を　webpack.loaders.js ファイルで行います。

```js:webpack.loaders.js
  ...
  {
    test  : /\.styl$/,
    loader: 'style!css!stylus'
  },
  ...
```

もともと css の設定が記述されていましたが、削除しました。

Stylus のファイルは `src/styles/` 以下に置きます。ルートとなる `main.styl` を src/index.jsx で import します。

```js:src/index.jsx
import './styles/main.styl';
```

今回の修正は、[こちらで確認できます](https://github.com/ogaoga/json-visual-editor/commit/19b1072b8d058bc291b7dc7de48c847260a1b448)。

追記：[構成をちょっと変えました](https://github.com/ogaoga/json-visual-editor/commit/fb0b64448d7d43afd521ac811dd876aad2333a1f)。

## CSS フレームワーク「Material Design Lite」のインストール

多少は綺麗なスタイルにしたいので、[Material Design Lite](https://getmdl.io/showcase/index.html) を導入。[Material-UI](http://www.material-ui.com/#/) という選択肢もあったけど、React との結びつきが強そうだったので、スタイルだけを利用できる MDL に。

```bash
$ npm install --save material-design-lite
```

でインストールして、Stylus で組み込む。

```css:src/styles/index.styl
@import "../../node_modules/material-design-lite/material.min.css"
@import url("https://fonts.googleapis.com/icon?family=Material+Icons")
```

JavaScript も。

```js:src/index.jsx
import '../node_modules/material-design-lite/material.min.js';
```

## ESLint のインストール（2016/06/01 追記）

まずは、ESLint のインストール。

```bash
$ npm install -g eslint
$ eslint --init
```

対話形式で設定ファイル（.eslintrc）を生成することができます。

Webpack から利用するので、[eslint-loader](https://github.com/MoOx/eslint-loader) もインストール。

```bash
$ npm install --save-dev eslint-loader
```

webpack.config.js で、eslint-loader を読み込む設定を追加（`preLoaders`の部分）。

```js:webpack.config.js
  module: {
    loaders: loaders,
    preLoaders: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: 'eslint-loader'
      }
    ]
  },
```

# React での UI の構築

ひとまず環境は整ったので、次に UI を構成してみます。出来上がったレイアウトはこちら。

![Screen Shot 2016-05-21 at 0.14.58.png](https://qiita-image-store.s3.amazonaws.com/0/4617/15119d0b-2d00-50d8-d353-d236952d17b8.png)

左側がテキストエリアで、ここに JSON を記述すると、右側のエリア（このスクリーンキャプチャーだとテキストが書いてあるエリア）に、その構造をあらわすテーブルが表示されます。テキストエリアには、JSON をコピーしたりクリアするボタン、JSON の文字数を示す文字列が存在します。

React のコンポーネント構成は下記の通り。

* [App](https://github.com/ogaoga/json-visual-editor/blob/4136021b004e82d4c33530f3f13c3c1530b5a975/src/index.jsx)
  * [Page](https://github.com/ogaoga/json-visual-editor/blob/4136021b004e82d4c33530f3f13c3c1530b5a975/src/Page.jsx)
      * [TextArea](https://github.com/ogaoga/json-visual-editor/blob/4136021b004e82d4c33530f3f13c3c1530b5a975/src/TextArea.jsx)
      * [ControlsArea](https://github.com/ogaoga/json-visual-editor/blob/4136021b004e82d4c33530f3f13c3c1530b5a975/src/ControlsArea.jsx)
          * Copy Button
          * Clear Button
          * Text Count
      * [VisualizedData](https://github.com/ogaoga/json-visual-editor/blob/4136021b004e82d4c33530f3f13c3c1530b5a975/src/VisualizedData.jsx)

まだ、UI の動作の実装、データのバインディングはできていません。この時点の[ソースコードはこちら](https://github.com/ogaoga/json-visual-editor/tree/c0d4f29d7813e40b2b6b767b5bd37074cfbcb78b)。

UI の起点となる [Page.jsx](https://github.com/ogaoga/json-visual-editor/blob/c0d4f29d7813e40b2b6b767b5bd37074cfbcb78b/src/Page.jsx) では、全体のレイアウトを定義しています。この中で、JSON を記入する [TextArea](https://github.com/ogaoga/json-visual-editor/blob/c0d4f29d7813e40b2b6b767b5bd37074cfbcb78b/src/TextArea.jsx)、操作用のボタン等を配置する [ControlsArea](https://github.com/ogaoga/json-visual-editor/blob/c0d4f29d7813e40b2b6b767b5bd37074cfbcb78b/src/ControlsArea.jsx)、記入された JSON をビジュアル化する [VisualizedData](https://github.com/ogaoga/json-visual-editor/blob/c0d4f29d7813e40b2b6b767b5bd37074cfbcb78b/src/VisualizedData.jsx) を組み込んでいます。

この構造がベストかどうかは現時点ではわかりません。今後、実装していく中で調整が入ると思われます。

# データのハンドリング

テキストエリアに記入した JSON を App コンポーネントで管理して、更新されるとそれに従って UI が更新されるようにします。

## データの定義

ルートコンポーネントである App でデータを保持するようにします。

```js:index.jsx
export class App extends React.Component {

  // コンストラクタでデータを保持する変数を定義。
  constructor(props) {
    super(props);
    this.state = {
      data: null
    };
    // this をバインド。
    this.updateData = this.updateData.bind(this);
  }

  // データを更新するためのメソッド。
  updateData(newData) {
    this.setState({data: newData});
  }

  // data でデータを渡して、updateData でデータ更新用メソッドを渡す。
  render() {
    return (
      <Page data={this.state.data} updateData={this.updateData} />
    );
  }
}

ReactDOM.render(<App/>, document.querySelector("#myApp"));
```

コンストラクタで定義した `data` を下記のように子コンポーネントに渡します。

```js:index.jsx
      <Page data={this.state.data} updateData={this.updateData} />
```

このように、Page 以下のコンポーネントに、`data` を渡します。同時に、TextArea で JSON が変更された時に `data` を更新するためのメソッド `updateData()` を渡して、データを更新できるようにします。

また、これは React とは関係ないですが、

```js:index.jsx
    // this をバインド。
    this.updateData = this.updateData.bind(this);
```

独自のメソッド内の `this` は `React.component` ではないので、`.bind()` で `this` が使えるようにします。（参考：[Why this.setState is undefined in React ES6 class? · Issue #283 · goatslacker/alt](https://github.com/goatslacker/alt/issues/283)）

## データの更新

Page に渡されたデータは、`this.props.data` でアクセスできます。これを、TextArea と VisualizedData に渡します（現時点では、TextArea に渡す必要はないですが）。また、TextArea には、データ更新用のメソッドも同様に渡します。

```js:Page.jsx
    ...
    <TextArea data={this.props.data} updateData={this.props.updateData} />
    ...
    <VisualizedData data={this.props.data} />
    ...
```

TextArea では、３秒ごと（暫定）に記入した JSON を VisualizedData で表示するように、渡された更新用メソッドを呼び出します。

```js:TextArea.jsx
export default class TextArea extends React.Component {

  constructor(props) {
    super(props);
    let text = (props.data === null) ? "" : JSON.stringify(props.data)
    this.state = {
      text: text
    };
  }

  componentDidMount() {
    setInterval((() => {
      let text = this.refs.jsonText.value;
      try {
        let data = JSON.parse(text);
        this.props.updateData(data);
      } catch(e) {
        console.log('Not JSON: '+text);
      }
    }).bind(this), 3000);
  }

  render() {
    return (
      <textarea placeholder="Write JSON code here."
                defalutValue={this.state.text}
                ref="jsonText"></textarea>
    );
  }
}
```

コンポーネントが DOM に追加された時に呼び出される `componentDidMount()` で周期的に `this.props.updateData()` を呼び出すようにしてデータを更新しています。`<textarea>` 内のテキストは `this.refs.jsonText.value` で呼び出していますが、これは `ref="jsonText"` と指定することで参照できるようになっています。

## 更新されたデータの表示

TextArea で `this.props.updateData()` を呼び出すと、App コンポーネントの `updateData()` が呼び出され、データが更新されます。

```js:index.jsx
  updateData(newData) {
    this.setState({data: newData});
  }
```

これにより、子コンポーネントに変更が伝わり、表示が更新されます。

```js:VisualizedData.jsx
export default class VisualizedData extends React.Component {
  render() {
    return (
      <div>{JSON.stringify(this.props.data)}</div>
    );
  }
}
```

VisualizedData では、渡されたデータを JSON の文字列に変換して表示しています。特に、更新を監視するような処理はありません。

今後、文字列ではなくデータ構造をテーブルで可視化する処理を追加します。

# データのビジュアライズ

VisualizedData にデータが渡ってきたので、これをテーブル形式で表示します。`Object` データをテーブルに変換するコンポーネント `ObjectType` はこんな感じです。

```js:ObjectType.jsx
export default class ObjectType extends React.Component {
  render() {
    let data = this.props.data;
    let result = null;
    if (data === null) {
      // null
      result = (<span className="null">null</span>);
    }
    else if (typeof(data) === typeof({}) && data !== null) {
      // Object or Array
      let rows = Object.keys(data).map((name) => {
        return (
          <tr>
            <th>{name}</th>
            <td><ObjectType data={data[name]} /></td>
          </tr>
        );
      });
      result = (
        <table>
          <tbody>
            {rows}
          </tbody>
        </table>
      );
    }
    else if (typeof(data) === typeof(1)) {
      // Number
      result = (<span className="number">{data}</span>);
    }
    else if (typeof(data) === typeof("a")) {
      // String
      result= (<span className="string">"{data}"</span>);
    }
    else if (typeof(data) === typeof(true)) {
      // Boolean
      result = (<span className="boolean">{(data)?'true':'false'}</span>);
    }
    else {
      // something else
      result = (<span className="undefined">{data}</span>);
    }
    return result;
  }
}
```

渡されたデータは `this.props.data` に入っていて、型を判断して Object 型の場合には、テーブル（`<table>`）で描画します。オブジェクトの各要素ごとにキー（配列の場合はインデックス番号）と値をテーブルの行として描画するのがこの部分。

```js
      // Object or Array
      let rows = Object.keys(data).map((name) => {
        return (
          <tr>
            <th>{name}</th>
            <td><ObjectType data={data[name]} /></td>
          </tr>
        );
      });
```

ここで、値を描画する部分で再帰的に `<ObjectType />` を呼び出しています。すべての行を `rows` 配列に保持したら、`<table>` で括って返します。

```js
      // result は render() の戻り値
      result = (
        <table>
          <tbody>
            {rows}
          </tbody>
        </table>
      );
```

これに[スタイルをあてる](https://github.com/ogaoga/json-visual-editor/commit/24cc46dc29d89a2db5d3a73d4119e83481ee0c95#diff-4e5c016ac6da0510082464764792c306)と、データがテーブル形式で表示されます。

ここまでで、記入した JSON がビジュアライズされるところまでできました。他にも細かい点が実装できていないですが、ひとまず GitHub Pages にデプロイしてみます。

# GitHub Pages へのデプロイ

Webpack は、ビルドするとアセットを bundle.js というファイルにパッキングします。現在の設定では、

```bash
$ npm run build
```

を実行すると、`public/` ディレクトリに（.map ファイルとともに）保存されます。この中にある index.html を開くと bundle.js が読み込まれて、アプリケーションとして動作します。

これを GitHub Pages に簡単にデプロイするために、[gh-pages](https://github.com/tschaub/gh-pages) を使います。

```bash
$ npm install gh-pages --save-dev
$ npm run build
$ gh-pages -d public
```

これだけで、https://ogaoga.github.io/json-visual-editor/ にデプロイされました！

~~この方法だと手動でデプロイする必要があるため、master ブランチに push すると自動的に [Travis CI](https://travis-ci.org/) でテストして、デプロイするようにしたいと思います。~~ Travis CI からデプロイするのは意外と大変そうなので後回しにしました。

# UI の状態コントロール

JSON の入力に応じて、データの表示だけではなく周辺の UI も状態を制御する必要があります。

## &lt;textarea&gt; を state で管理する。

最初の実装では、`<textarea>` に記入された文字列を `value` 属性で管理していなかったので、[React の公式ドキュメント](https://facebook.github.io/react/docs/forms-ja-JP.html)に沿って変更します。

```js:TextArea.jsx
        <textarea id="json-text"
                  placeholder="Write JSON code here."
                  value={this.state.text}
                  onChange={this.onChange}
                  ref="jsonText"></textarea>
```

これだけだと入力時に内容が更新されないので、`onChange` イベントで `onChange` メソッドを呼び出して、`value` を入力された文字列で更新する処理を追加します。

```js:TextArea.jsx
  onChange(event) {
    this.setState({text: event.target.value});
  }
```

これで、`<textarea>` の変更が伝播するようになります。

## コンポーネント構成を組み替える。

Copy ボタンや文字数カウンタなどを管理している ControlArea コンポーネントを、TextArea コンポーネントの子コンポーネントとなるように変更します。これにより、文字列を簡単に渡せるようになり、文字数の表示やボタンの disabled の制御が簡単になります。

```js:TextArea.jsx
        <ControlsArea text={this.state.text}
                      clearText={this.clearText} />
```

`text` 属性で入力されたテキストを渡すのと同時に、clearText というメソッドを追加して、Clear ボタンを押した時に文字列を空にできるようにします。

```js:TextArea.jsx
  clearText() {
    this.setState({text: ''});
  }
```

## 文字数カウントの実装

ControlArea に入力された文字列が渡されるようになったので、これをもとに文字数カウントを表示させます。

```js:ControlsArea.jsx
        <div className="float-right control-count">
          <span className="text-count">{this.props.text.length}</span>
        </div>
```

## ボタンの disabled の制御

Copy ボタンと Clear ボタンは、文字列が入力されている時だけ有効なので、disabled 属性にこの条件を与えてボタンを制御します。

```js:ControlsArea.jsx
          <button id="copy-to-clipboard"
                  data-clipboard-target="#json-text"
                  disabled={this.props.text.length==0}
                  className="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect">Copy</button>
```

## Clear ボタンの実装

Clear ボタンを押すと文字列が空になるようにするために、TextArea コンポーネントでそのメソッドを定義して、そのインタフェースを ControlsArea に渡します。これを、`<button>` の `onClick` で呼び出すことで、文字列を空にできます。

```js:ControlsArea.jsx
          <button className="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect"
                  disabled={this.props.text.length==0}
                  onClick={this.props.clearText}>Clear</button>
```

このように、あくまで渡された文字列データをもとに各種の制御を行い、`<textarea>` との依存関係をつくらないことがポイントです。また、親コンポーネントのデータを更新したい場合は、そのインタフェースを渡して、直接操作させないことも重要です。

これらの変更は[こちらをご確認ください](https://github.com/ogaoga/json-visual-editor/commit/712858b0daf21fc0b18671a84eb46062b482fa44)。

# Redux の導入

UI コンポーネントが増えてきたり構造が複雑になってくると、ルートのコンポーネントで管理しているデータを操作するメソッドを props で渡したり、コンポーネント間で state のデータをやり取りするのが面倒になってきます。また、全然依存がないコンポーネントがデータやメソッドをバケツリレーしなければならないのも気持ち悪いです。

これを解決するために、Redux というフレームワーク導入します。これにより、各コンポーネントの state で管理していたデータを一元管理して、そのデータを操作するメソッドをどのコンポーネントからでも呼び出せるようにして、依存関係を減らします。

Redux の導入はこれだけでも長くなるので、別の記事に分けました。こちらも併せてご覧ください。

[React + ES6 + Webpack で JSON Visual Editor を作ってみる（Redux 導入編） - Qiita](http://qiita.com/ogaoga/items/e3f7e6d1d3aeb61351f5)

----

___現在はここまで。開発の進捗に沿って追記していきます。___

----

# 参考

* [Webpack + React + ES6の最小構成を考えてみる。 - TOEIC940点の文系大学生プログラマによるブログ](http://uraway.hatenablog.com/entry/2015/12/25/Webpack_%2B_React_%2B_ES6%E3%81%AE%E6%9C%80%E5%B0%8F%E6%A7%8B%E6%88%90%E3%82%92%E8%80%83%E3%81%88%E3%81%A6%E3%81%BF%E3%82%8B%E3%80%82)
* [ReactのチュートリアルをES6で書いてwebpackとESLintも使ってみる - Qiita](http://qiita.com/morizotter/items/9e2a7def6773a2a8e174)
* [webpack.config.jsの読み方、書き方 - dackdive's blog](http://dackdive.hateblo.jp/entry/2016/04/13/123000)
* [npm run deploy で GitHub Pages にお手軽デプロイ - Qiita](http://qiita.com/star__hoshi/items/490959aee12dbf528f7c)
* [ReduxとES6でReact.jsのチュートリアルの写経 - undefined](http://blog.bokuweb.me/entry/redux-tutorial)