gulp-jasmineでES6で書いたReactコンポーネントとReactステートレスコンポネントさらにはExpressルーターを同時にテストできるようにする

一通りハマって模索した結果、それぞれしっかり原因判明やりきって解決するまでの質ではないのですが、ES6 で書いたReactコンポーネントとステートレスコンポーネントをテストするとともに、ES6で書いたExpressルーターもまとめてテストできるプロジェクト設定・構成が一つ用意できました。

まず前提。

  • ES6で書いて、Babelでトランスパイルしている
    • jestで作ったテスト環境では、SuperTestを用いたExpressルーターテストがどうにも動かなかった
    • jest.autoMockOff()をかけても、あちらこちらでモックになっちまう。
    • 先にBabelトランスパイルしてから、出力ファイルでjest実行してみてもなぜか改善せず
    • jestは難しいことを簡単にできて素敵に思っていたけど、意外に簡単と思ってたところでエラーが出始め。解決がどうにも難しくてjestにギブアップしてしまった
  • Reactステートレスコンポーネントはテストが動かない。
    • 普通のReactコンポーネントはテスト可能
    • 後で判明するけどこれはjestのせいじゃなかった

ぐるぐる回ってみて、ちょっとjestでは層が深くて追いきれなくなってきたのでJasmine2でくみなおしました。もちろんいきなり結論には辿りつけずにいろいろ入れたり外したりしましたよ。。。以下は、シンプル化したpackage.jsonです。Reactテストには、gulp-jasmineとreact-addons-test-utilsとjsdomが関わります。Expressテストの方ではsupertest。ビルド及びテストの前処理は、gulp-babelとbabel-preset-*。

{
  "name": "sample",
  "version": "0.0.1",
  "private": true,
  "dependencies": {
    "express": "^4.13.4",
    "react": "^0.14.7",
    "react-dom": "^0.14.7"
  },
  "devDependencies": {
    "babel-preset-es2015": "^6.5.0",
    "babel-preset-react": "^6.5.0",
    "gulp": "^3.9.1",
    "gulp-babel": "^6.1.2",
    "gulp-jasmine": "^2.2.1",
    "jsdom": "^8.0.2",
    "react-addons-test-utils": "^0.14.7",
    "supertest": "^1.2.0"
  },
  "babel": {
    "presets": [
      "es2015",
      "react"
    ]
  },
  "scripts": {
    "build": "gulp build",
    "start": "cd dest; node app.js",
    "test": "gulp test"
  }
}

gulpでトランスパイルもしくはテスト実行という流れで、今後、もうチョイと便利に改良していく予定。gulpfile.jsだけはES5で書いてます。

// gulpfile.js
'use strict';
var gulp = require('gulp');
var babel = require('gulp-babel');
var jasmine = require('gulp-jasmine');
var reporter = require('jasmine-terminal-reporter');
var jsdom = require('jsdom').jsdom;
require('babel-register'); // Jasmineで事前トランスパイルなしに動かすために必要

gulp.task('build', function() {
    gulp.src(['src/**/*.js', '!src/**/__tests__/*.js']) //テストは出力しない
        .pipe(babel())
        .pipe(gulp.dest('dest'));
});

gulp.task('test', function () {
    global.document = jsdom(''); // 空白文字列でOK。好きなHTMLを書いても支障なし。
    global.window = global.document.defaultView; // defaultViewにするのは最近の仕様変更。
    for (var key in global.window) {
        if (global.window.hasOwnProperty(key) && !global[key]) {
            global[key] = global.window[key]; //念のため上書きなしで参照をコピーしておく
        }
    }
    gulp.src('src/**/__tests__/*.js')  //テストの配置方法は直前まで試していたjestの名残りです。
        .pipe(jasmine({reporter: new reporter()}));
});

トランスパイルの方はいいとして、テストの方について。Reactのテストはやり方によってはDOMが要らないやり方もあるみたいですが、jestを通じて知ったTestUtilsのrenderIntoDocumentを使う直感的なビヘイビアテストをするために、JsDOMでヘッドレスなDOM環境を作ります。その際、テストのルートたるgulpタスクで作っちゃった。コード中で「typeof window === 'undefined'」とサーバ環境判定していたりするとダメですが、これだけは自分で書かないと決めりゃいいかな。jestはざっくりとその辺はうまくやってるっぽい(jest/JSDOMEnvironment.js at master · facebook/jest · GitHub と jest/NodeEnvironment.js at master · facebook/jest · GitHub)。

ちなみに、React Test Utilsのドキュメントの該当箇所「NOTE」を読んでも全くわからなかったため相当費やしましたよ。。。解決のためにReactだけでなくjestやJsDOMの見当違いなソース箇所ばかり読んでた(Test Utilities | React)。

node環境のグローバルスコープの名前であるglobalのプロパティとしてdocumentを作り、windowを付け足します。その後にfor...inループではwindowからglobalへ上書きしないようにしながら参照をコピーしました。本当はnavigatorだけ手当てすれば良いみたいですけど念のためです。次いでgulp-jasmineでsrcフォルダのテストを一切実行します。jestの名残でテストのフォルダは各所の__tests__フォルダとなってます。

buildタスク、testタスクともにBabelのプリセット設定はpackage.jsonを使ってくれてますのでES6とJSXどちらも対応しています。

'use strict';
import React from 'react';

class CheckboxWithLabel extends React.Component {
    constructor(props) {
        super(props);
        this.state = {isChecked: false};
    }

    handleChange() {
        this.setState({isChecked: !this.state.isChecked});
    }

    render() {
        const sender = this;
        return (
            <label>
                <input
                    type="checkbox"
                    checked={this.state.isChecked}
                    onChange={() => sender.handleChange()}
                />
                {this.state.isChecked ? this.props.labelOn : this.props.labelOff}
            </label>
        );
    }
}

CheckboxWithLabel.propTypes = {
    labelOn: React.PropTypes.string.isRequired,
    labelOff: React.PropTypes.string.isRequired
};

export default CheckboxWithLabel;

上記のテスト対象はjestでのチュートリアルのものです。ESLintに怒られたのでちょっと整えました。

'use strict';
import React from 'react';
import ReactDOM from 'react-dom';
import TestUtils from 'react-addons-test-utils';
import Express from 'express';
import SuperTest from 'supertest';
import CheckboxWithLabel from '../CheckboxWithLabel';
import {ResponsiveCol} from '../BootstrapResponsive';
import WorkableRouter from '../WorkableRouter';

describe('React Components and Express' , () => {
    it('changes the text after click', () => {
        var checkbox = TestUtils.renderIntoDocument(
            <CheckboxWithLabel labelOn="On" labelOff="Off" />
        );
        var checkboxNode = ReactDOM.findDOMNode(checkbox);
        expect(checkboxNode.textContent).toEqual('Off');
        TestUtils.Simulate.change(
            TestUtils.findRenderedDOMComponentWithTag(checkbox, 'input')
        );
        expect(checkboxNode.textContent).toEqual('On');
    });

    // 下はReactステートレスコンポーネントのために動かない。
    it('hidden setting', () => {
        const col = TestUtils.renderIntoDocument(
            <ResponsiveCol xs={{hidden: true}} sm={{hidden: true}} md={{hidden: false}}>
                <div></div>
            </ResponsiveCol>
        );
        const colNode = ReactDOM.findDOMNode(col);
        expect(colNode.getAttribute('class').trim()).toEqual('hidden-xs hidden-sm');
    });

    it('get jobs', (done) => {
        const app = Express();
        app.use(WorkableRouter);
        SuperTest(app)
            .get('/jobs')
            .expect(200, done);
    });

});

もちろんテストもjestから。これと2/10の記事に書いたReactステートレスコンポーネントのテストも並べておきます。さらにSuperTestでルータの非同期テストも。

F..

events.js:154
      throw er; // Unhandled 'error' event
      ^
Failures: 
Error: Tests failed
1) React Components and Express hidden setting
1.1) TypeError: Cannot read property 'getAttribute' of null

3 specs, 1 failure
Finished in 0.4 seconds

結果は、ES6-classのReactコンポーネントはOK、SuperTestはOK。でもReactステートレスコンポーネントはNG。TestUtils.renderIntoDocumentでnullを返してます。しばしトランスパイルされた結果を眺めていて気がつきました。そこで下記の工夫。

    it('hidden setting', () => {
        const Component = ResponsiveCol({xs: {hidden: true}, sm: {hidden: true}, md: {hidden: false}, children: ''});
        const col = TestUtils.renderIntoDocument(Component);
        const colNode = ReactDOM.findDOMNode(col);
        expect(colNode.getAttribute('class').trim()).toEqual('hidden-xs hidden-sm');
    });

3種とも通りました!ステートレスコンポーネントはこう書けばいいのね。ステートレスコンポーネントは引数にプロパティを渡す単なる関数で、戻りはReact ElementになるのでそのままrenderIntoDocumentに渡してあげればよかった!まあrenderIntoDocumentの実装が追いついていないってことで確定だと思います。

...

3 specs, 0 failures
Finished in 0.3 seconds

そろそろgulpでちゃんとしたワークフローを組むべきかな。gulpのgithubにレシピ集があった( gulp/docs/recipes at master · gulpjs/gulp · GitHub)。いろいろ参考になる。

react-router v2リリース

しばらくrc版を用いて開発していましたが、react-router v2が昨日か一昨日ぐらいにリリースされていました。ドキュメントも新しくv2になってます。

react-router/ComponentLifecycle.md at latest · rackt/react-router · GitHub

いつからあったかは定かではないですが、コンポーネントライフサイクルに関するドキュメントが良い情報。これはv2に限らず以前のバージョンでも同じ。パス遷移した時に各ページを構成するコンポーネントのどんなライフサイクルイベントが発火されるかの説明です。説明されるまでもなく慣れると体験的に知ってることなのですが、初めにこのドキュメントを読んでたら近道だったように思います。

react-router/testing.md at latest · rackt/react-router · GitHub

テストも。Jestのユースケースとして初見で参考になる。

TestUtilsが関数コンポーネントダメだって

'use strict';
/*global jest*/
jest.dontMock('../BootstrapResponsive');
const React = require('react');
const ReactDOM = require('react-dom');
const TestUtils = require('react-addons-test-utils');
import {ResponsiveCol} from '../BootstrapResponsive';
describe('Col', () => {
    it('hidden setting', () => {
        const col = TestUtils.renderIntoDocument(
            <ResponsiveCol xs={{hidden: true}} sm={{hidden: true}} md={{hidden: false}}/>
        );
        const colNode = ReactDOM.findDOMNode(col);
        expect(colNode.getAttribute('class').trim()).toEqual('hidden-xs hidden-sm');
    });
});

Jestで後付けにテスト書いたら、TestUtilsが関数コンポーネント(ステートレスコンポーネント)に対応していなくてダメだって。

Using Jest CLI v0.8.2, jasmine1
Running 1 test suite...Warning: ResponsiveCol(...): No `render` method found on the returned component instance: you may have forgotten to define `render`, returned null/false from a stateless component, or tried to render an element whose type is a function that isn't a React component.

「you may have fogotten to define 'render'」って、忘れてないよ!わざとだよ!「returned null/false from a stateless component」って。。。どう言うこっちゃ?

というところで、これはテストそのものが動かなかったけど、Jestは便利です。以下設定備忘。

babel, babel-preset-es2015, babel-preset-reactはすでに入ってる前提でJestインストール。

$ npm install --save-dev jest-cli babel-jest react-addons-test-utils

package.jsonにJestの設定追加。

{
  "babel": {
    "presets": [
      "es2015",
      "react"
    ]
  },
  "jest": {
    "scriptPreprocessor": "<rootDir>/node_modules/babel-jest",
    "unmockedModulePathPatterns": [
      "<rootDir>/node_modules/react",
      "<rootDir>/node_modules/react-dom",
      "<rootDir>/node_modules/react-addons-test-utils",
      "<rootDir>/node_modules/fbjs"
    ]
  },
  "scripts": {
    "test": "jest"
  }
}

で、あとはソースツリーの書きたいところで、「__tests__」フォルダを作ってその中に.jsファイルを置く。ファイルの中身は冒頭のようなのです。es6もJSXも設定で有効にしてます。中身がJasminなのでJasminな書き方にJest特有のモック自動生成、ReactアドオンTestUtilsのツールで仮想DOM-テストDOMの連携動作が乗っかります。

リファレンスは以下。

リファレンスまとめて気がついた。Jasminは1.3.0で古い。node_modules配下のjest-cliを見ると腹にjasmin2.3.4を抱えてたので切り替える設定がありそう。

追記

。。。切り替え設定ありました。

https://facebook.github.io/jest/docs/api.html#config-testrunner-string

さっきの設定に一行足します。

  "jest": {
    "scriptPreprocessor": "<rootDir>/node_modules/babel-jest",
    "testRunner": "<rootDir>/node_modules/jest-cli/src/testRunners/jasmine/jasmine2.js",
    "unmockedModulePathPatterns": [
      "<rootDir>/node_modules/react",
      "<rootDir>/node_modules/react-dom",
      "<rootDir>/node_modules/react-addons-test-utils",
      "<rootDir>/node_modules/fbjs"
    ]
  },

実行したら、ちゃんと2だって言ってる。今まで1.3でも支障なかったけど今日から2.3にしておこう。

Using Jest CLI v0.8.2, jasmine2
Running 1 test suite...

BootstrapのGridをReactに持ってきてみた。

material-uiを気にいって、それでReactアプリ書いてます。発展途上ではあるも開発者の方々がこまめにがっつり頑張ってくれてて、日々npm updateをかけるのが楽しみです。そんなmaterial-uiも今の所はGridレイアウトのコンポーネントはありません。Google謹製マテリアルデザインのCSSライブラリであるMaterial Design Lightは画面幅を12分割するというBootstrap近似仕様のGridレイアウトがありますので、まあ、対抗してmaterial-uiにもそのうち作られるかな。

Reactコンポーネントとしてもいくつかあるようですが、今の所良さそうなものが見つからなかったので暫定的にBootstrapを持ち込んで軽め対応してみました。スタイルはインラインで書くようにしていたので、Gridだけとはいえcssファイルに外だしちゃうのがちょっと嫌だったけど、手もかけたくなかったので素に近い使い方に止めてます。

$ npm install --save bootstrap-sass

いろいろ試行錯誤してみた結果、SASS版Bootstrapから必要箇所を取り込んで使うことにしました。上記npmインストールでnodo_modules配下にSASSソースの最新版が管理されます。

/* /src/sass/style.scss */
@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap-sprockets";
@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/variables";
@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/mixins";
@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/grid";
@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/scaffolding";
@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/responsive-utilities";

自分のスタイルシートを作って必要なだけimport。これをgulp-sassでコンパイル

// gulpfile.js
'use strict';
var gulp = require('gulp');
var sass = require('gulp-sass');

gulp.task('SASS', function() {
    gulp.src('src/sass/*.scss')
        .pipe(sass())
        .pipe(gulp.dest('dest/public/css'));
});

コンポーネントはcontainer-row-colの組み合わせ。

// BootstrapResponsive.js
'use strict';
const React = require('react');

function ResponsiveContainer(props) {
    return (
        <div className={props.fullWidth ? 'container-fluid' : 'container'}>
            {props.children}
        </div>
    );
}

ResponsiveContainer.propTypes = {
    fullWidth: React.PropTypes.bool,
    children: React.PropTypes.node.isRequired
};

function ResponsiveRow(props) {
    return (
        <div className='row'>
            {props.children}
        </div>
    );
}

ResponsiveRow.propTypes = {
    children: React.PropTypes.node.isRequired
};

function ResponsiveCol(props) {
    const className = ['xs', 'sm', 'md', 'lg'].map((type) => {
        const value = props[type];
        var result = [];
        if(value) {
            var valueSize = 0;
            if (typeof value === 'number') {
                valueSize = value;
            } else {
                const {size, offset, visible, block, inline, hidden} = value;
                valueSize = size;
                if (0 <= offset && offset <= 12) {
                    result.push('col-' + type + '-offset-' + offset);
                }
                if (visible) {
                    result.push('visible-' + type + '-' + (inline ? (block ? 'inline-block' : 'inline') : 'block'));
                }
                if (hidden) {
                    result.push('hidden-' + type);
                }
            }
            if (1 <= valueSize && valueSize <= 12) {
                result.unshift('col-' + type + '-' + valueSize);
            }
        }
        return result.length > 0 ? result.join(' ') : '';
    }).join(' ');
    return (
        <div className={className}>
            {props.children}
        </div>
    );
}

const responsivePropsType = React.PropTypes.oneOfType([
    React.PropTypes.number,
    React.PropTypes.object
]);

ResponsiveCol.propTypes = {
    xs: responsivePropsType,
    sm: responsivePropsType,
    md: responsivePropsType,
    lg: responsivePropsType,
    children: React.PropTypes.node.isRequired
};

export {ResponsiveContainer, ResponsiveRow, ResponsiveCol};

今回はステートレスなコンポーネントだったのでシンプルに関数で書いてみました。公式ドキュメントReusable Components | React)の説明以外に見ない書き方ですが不要なお決まりイディオム抜きで良い感じだと思います。今回は一発で書いちゃってますがロジックを細かく関数分割すればテストもしやすいでしょう。

およそBootstrapのGridシステムについてのドキュメント(CSS · Bootstrap #gridと CSS · Bootstrap #responsive-utilities-classes)に書かれていたものの8割ぐらいは実装しました。まあdivタグにクラス文字列埋めるだけですからね。残したのはclearfixとpull/push。やっても直接divタグ書くのと何が違うのかって感じだからもっと抽象度を高める利用アイディアなきゃいらないかなと思います。

const React = require('react');
import {Card, CardText} from 'material-ui';
import {ResponsiveContainer, ResponsiveRow, ResponsiveCol} from './BootstrapResponsive';

/*中略*/

<ResponsiveContainer fullWidth={true}>
    <ResponsiveRow>
        <ResponsiveCol xs={{size: 12}} sm={6} md={{size: 8, offset: 2}} lg={{size: 4, offset: 0}}>
            <Card>
                <CardText>
                    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
                    Donec mattis pretium massa. Aliquam erat volutpat. Nulla facilisi.
                    Donec vulputate interdum sollicitudin. Nunc lacinia auctor quam sed pellentesque.
                    Aliquam dui mauris, mattis quis lacus id, pellentesque lobortis odio.
                </CardText>
            </Card>
        </ResponsiveCol>
        <ResponsiveCol xs={{hidden: true}} sm={6} md={{size: 8}} lg={{size: 4}}>
            <Card>
                <CardText>
                    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
                    Donec mattis pretium massa. Aliquam erat volutpat. Nulla facilisi.
                    Donec vulputate interdum sollicitudin. Nunc lacinia auctor quam sed pellentesque.
                    Aliquam dui mauris, mattis quis lacus id, pellentesque lobortis odio.
                </CardText>
            </Card>
        </ResponsiveCol>
    </ResponsiveRow>
</ResponsiveContainer>
  • xsサイズでは全幅の一つと、もう一つは消す。
  • smサイズでは横二分割。
  • mdサイズでは左右余白2で8幅。縦に並べる。
  • lgサイズでは4幅で横に並べる。

やってることは原始的だけど意外に使える。外出しになってるBootstrapのCSSコンポーネントにインライン化できればいいんじゃないかな。css擬似要素とメディアクエリをインライン縛りにどう移植するか。。。擬似要素はspanを動的に足してくかだな。メディアクエリはScriptで幅をとって計算。。。サーバ描画でうまくないなあ。

ReactのES6-classでのコンテキスト

class PutContext extends React.Component {
    getChildContext() {
        return {color: '#03a9f4'};
    }
    render() {
        return <div>{this.props.children}</div>;
    }
}
// childContextTypesはpropTypesと同じスタイルの設定
PutContext.childContextTypes = {color: React.PropTypes.string};

class GetContext extends React.Component {
    // コンストラクタの第二引数にcontextが渡され、コンテキストを利用したstate初期化が可能
    constructor(props, context) {
        super(props, context);
        this.state = {
            color: context.color
        };
    }

    render() {
        const color = this.state.color;
        return <div style={{color: color}}>the context color is {color}.</div>;
    }
}
// contextTypesもpropTypesと同じスタイルの設定
GetContext.contextTypes = {color: React.PropTypes.string}

react/ReactComponent.js at master · facebook/react · GitHub

上記リンク先、React.Componentのソースを見るとドキュメントに明らかな第一引数のpropsだけでなくコンストラクタには第二、第三の引数があってコンテキストが渡されてきてました。よってES6-classを用いたときに従来のgetInitialState()をコンストラクタ内の処理で置き換える際にコンテキストを参照することが可能。

WebStormでESLintを使う

数日、バタバタと調査しては考えが変わる毎日ですが、ES6-classでReactアプリを書く前提が整ってきました。

  • classボディ外に、propTypes/contextTypes/childContextTypesを書くのも、そういうものと思えばまあいいか
  • MixinはAOP的なラッパーコンポーネントを生成する関数で書く。移行もすでにES6記法で書いてたのでさほど面倒ではない
  • contextはES6-classでも大丈夫だった
  • BabelでES5へトランスパイルし、Browserifyでクライアント用はまとめる。
  • テスト周りがちょっと試行不十分だけど、Jest&Babel&Gulpで簡単なものはOK

おそらく最後にして最大の課題はIDEのことです。コード的にはOKでもIDEがエラーや警告だしたり、逆に出すべきところで沈黙してたりすると書き方云々以上にストレスですから。今、私はWebStorm11をサブスクリプション購入してますのでそこでの設定。Preferencesを眺めてちょこちょこ試したらうまくいった(ドキュメントはあるけど、それだけ読んで調べられるかというと、込み入ったことは進化が早すぎるからか追いついていないようで期待できないし、恐ろしいことに有料の商用IDEだからなのかググっても情報がなかなかでてこない)。

f:id:masataka_k:20160207054826p:plain

まず、WebStorm標準のソースインスペクタを切る。[Editor]->[Inspections]->[JavaScript]のチェックを外す。画像は[Code quality tools]->[ESLint]がチェックされているけどこれは後の作業で自動でチェックされます。

すでにBabel/BabelのReact及びES2015プリセットが入ってる前提で、以下のnpmインストールを実行。

$ npm install --save-dev eslint
$ npm install --save-dev eslint-plugin-react
$ npm install --save-dev babel-eslint

ちなみに私のpackage.jsonのdevDependenciesは今日時点で以下のようになってます。全部プロジェクトのnode_modulesに入れるようにしていて、グローバルにはnpmとnしか入れてません。それが正しいのか正しくないのかは知らない。

{
  "devDependencies": {
    "babel-eslint": "^4.1.8",
    "babel-jest": "^6.0.1",
    "babel-preset-es2015": "^6.3.13",
    "babel-preset-react": "^6.3.13",
    "babelify": "^7.2.0",
    "browserify": "^12.0.2",
    "eslint": "^1.10.3",
    "eslint-config-standard": "^4.4.0",
    "eslint-plugin-react": "^3.16.1",
    "eslint-plugin-standard": "^1.3.1",
    "gulp": "^3.9.0",
    "gulp-babel": "^6.1.2",
    "gulp-sass": "^2.2.0",
    "gulp-zip": "^3.1.0",
    "jest-cli": "^0.8.2",
    "react-addons-test-utils": "^0.14.6",
    "vinyl-source-stream": "^1.1.0"
  }
}

f:id:masataka_k:20160207055059p:plain

[Languages & Frameworks]->[JavaScript]->[Code quality tools]->[ESLint]をEnableにチェック!NodeとESLintの場所を適切に設定する。

プロジェクトルートに.eslintrc.jsonを配置します。設定ウィザードをコマンドラインで「eslint --init」実行してみたけど大したもの吐いてくれなかったので結局はググって拾ってきたのをいじりました。以下、何も省略せずに今日の私の設定です。ポイントは"parser"にbabel-eslintを設定しているところと"plugins"にreactを追加しているところ。eslint標準のパースに任せるとどうも何もおかしくないところで構文解析エラーを出したりするのでうまくない。先にnpmインストールしているbabel-eslintが全部キッチリと一本化して素敵なようす。

// .eslintrc.json
{
    "parser": "babel-eslint",
    "extends": "eslint:recommended",
    "env": {
        "es6": true,
        "node": true,
        "jasmine": true,
        "browser": true
    },
    "ecmaFeatures": {
        "jsx": true,
        "experimentalObjectRestSpread": true
    },
    "plugins": [
        "react"
    ],
    "rules": {
        "strict": [2, "global"],
        "no-const-assign": 2,
        "no-console": 0,
        "no-unused-vars": 1,
        "indent": [2, 4],
        "quotes": [2, "single"],
        "linebreak-style": [2, "unix"],
        "semi": [2, "always"],
        "react/display-name": 0,
        "react/forbid-prop-types": 0,
        "react/jsx-boolean-value": 0,
        "react/jsx-closing-bracket-location": 0,
        "react/jsx-curly-spacing": 1,
        "react/jsx-equals-spacing": 1,
        "react/jsx-handler-names": 1,
        "react/jsx-indent-props": 0,
        "react/jsx-indent": 1,
        "react/jsx-key": 1,
        "react/jsx-max-props-per-line": 0,
        "react/jsx-no-bind": [1, {"allowArrowFunctions": true}],
        "react/jsx-no-duplicate-props": 1,
        "react/jsx-no-literals": 0,
        "react/jsx-no-undef": 1,
        "react/jsx-pascal-case": 1,
        "react/jsx-quotes": 1,
        "react/jsx-sort-prop-types": 0,
        "react/jsx-sort-props": 0,
        "react/jsx-uses-react": 1,
        "react/jsx-uses-vars": 1,
        "react/no-danger": 0,
        "react/no-deprecated": 1,
        "react/no-did-mount-set-state": 1,
        "react/no-did-update-set-state": 1,
        "react/no-direct-mutation-state": 1,
        "react/no-is-mounted": 1,
        "react/no-multi-comp": 0,
        "react/no-set-state": 0,
        "react/no-string-refs": 1,
        "react/no-unknown-property": 1,
        "react/prefer-es6-class": 0,
        "react/prop-types": 1,
        "react/react-in-jsx-scope": 1,
        "react/require-extension": 1,
        "react/self-closing-comp": 1,
        "react/sort-comp": 0,
        "react/wrap-multilines": 1
    }
}

以下、発見と感想。

  • "react/jsx-boolean-value" は意味あんのかな?true縛りの記述にしてfalseは書かせないとか。。。
  • "react/jsx-no-literals" はスタイルとしてよりダメな方かなと。まあどちらでもいいけど
  • "react/jsx-no-bind" は全部禁じちゃうと手が出なくなる。 [1, {"allowArrowFunctions": true}]にしました。このことによって、bind(this, "flag")を使わなくても onClick={e => this.handleClick("flag", e);} ってできるようになる。
  • "react/no-string-refs" は謎でなく目うろこだった。これまでref="foo"だったところを、ref={ref => this.foo = ref}ってref-callbackを使うと。このref-callbackはReactの公式ドキュメントにちゃんと書いてあった! (https://facebook.github.io/react/docs/more-about-refs.html)Stringで名前つけて参照するのと違って堅くなっていいね!
  • "react/no-set-state" は、this.setState({foo: bar}); と言う状態操作を禁じる。禁じるとユーザーリアクションのあるアプリ書けないと思うんですけど?私にはまだ謎。
  • 最後に"react/prefer-es6-class"!。私はこれを1に設定しました。ES6-classを使わないと警告。今はたくさん警告出てます。

ESLintサイコー。細かく警告やエラーに調整できた上で、ソースコードではコメントで部分抑制かけられるので堅め設定しておいて適宜明示的に緩めるというのがサイコー。WebStormの標準インスペクタでもなく、ESLintの標準パーサーでもなく、eslint-babelにするってのがまたサイコー。

eslint.org

Reactコンポーネント名の規則

つまらないハマりがあったので。

const foo = React.createClass({
    render() {
        return <div>Foo</div>;
    }
});
console.log("JSX:" + ReactDOMServer.renderToStaticMarkup(<foo/>));
console.log("API:" + ReactDOMServer.renderToStaticMarkup(React.createElement(foo)));

上記のようなコードがあった時、出力は以下のようになりました。

JSX: <foo></foo>
API: <div>Foo</div>

JSXをBabelで変換していますが、こちらは「React.createElement("foo", null)」と変換してしまい、結果としてそのままマークアップに出てきちゃう。原因はJSXで先頭小文字の名前のコンポーネントをHTMLタグと認識してトランスパイルしちゃうから。回避方法はReactコンポーネント命名規則として先頭大文字にするということです。