[React.js] setStateしても直ちにstateは変更されない

最近仕事でReactをやっている。
今回のネタはひとこと(ひとコード?)で言うと以下のようなこと。

getInitialState () {
  return { clicked: false };
},
clickHandler () {
  this.setState({ clicked: true });
  console.log(this.state.clicked); // -> false
},
render () {
  console.log(this.state.clicked); // -> true
  return <button onClick={this.clickHandler}>Click Me</button>;
}

インタラクションのハンドラで this.setState したとして、そのハンドラ内でthis.state を参照しても、まだ更新後の値は取れない。render() が呼ばれるタイミングでは更新後の値になっている。実際にステートが更新されるタイミングは意図的に制御されているらしい。トリッキーだ。

更新後の値は変数に入れておいて this.setState しつつ、ハンドラ内ではその変数の値を使うのがおそらく正解だろう。

getInitialState () {
  return { clicked: false };
},
clickHandler () {
  var value = true;
  this.setState({ clicked: value });
  console.log(value); // -> true
},
render () {
  console.log(this.state.clicked); // -> true
  return <button onClick={this.clickHandler}>Click Me</button>;
}

Chrome 43 からは DOM attribute が prototype プロパティに変更される

DOM Attributes now on the prototype chain

それによって…

  • hasOwnProperty()/getOwnPropertyDescriptor()/JSON.stringify() で、DOM attribute が検出できなくなる
  • read-only な DOM attribute を上書きしようとすると例外を投げるようになる

だそうな。
他人が作ったフレームワークは特に、それで動かなくなったりするものも出てくるかもしれないので頭の片隅に置いておくとよい。

grunt-contrib-jshint の設定

Gruntfile.coffee に以下のように書く。

grunt.loadNpmTasks 'grunt-contrib-jshint'
grunt.initConfig(
  jshint:
    main:
      options:
        jshintrc: true
      src: [ 'app' + sitePath + 'js/script.js' ]
  ...
  watch:
    js:
      options:
        livereload: true
      files: [ 'app' + sitePath + 'js/*' ]
      tasks: [ 'jshint' ]
)

options.jshintrc = true を設定するとそれ以外の options は無視され同階層に置かれた .jshintrc ファイルを参照するようになる。

.jshintrc はJSON形式で記述する。
結果的に以下のような設定に。

{
  "node": true,
  "esnext": true,
  "bitwise": true,
  "camelcase": true,
  "curly": true,
  ...
  "globals": {
    "window": true,
    "document": true,
    "jQuery": true,
    "$": true,
    "_": true,
    "Backbone": true
  },
  "-W116": true,
  "-W041": true
}

globals には「XXXグローバルオブジェクトがない」的な警告が出るときに、予め「このグローバルオブジェクトはあるから、警告出すな」とJSHint側に伝えておく設定。
-WXXX: true は特定のエラーを無視したいときに追加する設定。このエラーコード(ドキュメントの原文では warning code)は grunt --verbose(もしくは grunt -v)として verbose モードで Grunt タスクを起動すると表示されるようになる。

表示例:

^ [W116] Expected '!==' and instead saw '!='.

ここでは W116 というのが warning code なのでそれを "-W116": true のように options に追加すると、警告は出なくなる。

js-test-driverの使い方メモ

js-test-driverはGoogleが作った、JavaScriptをコンソールでテストするプログラム。試したのでメモを。環境はMountain Lion搭載のMacBookAir。

ダウンロード:
http://code.google.com/p/js-test-driver/downloads/list

使い方:
http://code.google.com/p/js-test-driver/wiki/GettingStarted

参考になる日本語記事:
http://0-9.tumblr.com/post/15614207218/js-jstestdriver
http://everyday-eachday.blogspot.jp/2011/12/jstestdriver.html

ダウンロードページで JsTestDriver-x.x.x.jar ってやつを落とす。ダウンロードしたファイルを ~/bin/JsTestDriver.jar みたいなパスに置く。コンソールから以下のコマンドでサーバーを起動。

$ java -jar ~/bin/JsTestDriver.jar --port 4224

ポート番号は適当でいいらしい。コマンド成功したら、適当なブラウザで http://localhost:4224/ を開く。そうすると

  • Capture This Browser
  • Capture This Browser in strict mode

ってテキストリンクが並んでる画面になるので好きな方をクリック。(普通は Capture This Browser でいいと思われ)そうするとそのブラウザをウォッチしているような状態になる。で、そのブラウザのエンジンでテストが実行される。

実際にテストしたいプロジェクトのディレクトリを作る。例えば以下のような構造にする。

|- jsTestDriver.conf
|- src
|--- hoge.js
|- test
|--- hoge_test.js

hoge.js が書きたいJavaScript。hoge_test.js がhoge.jsに対するテストを書くファイル。jsTestDriver.conf がjs-test-driverのためのファイル。テキストエディタで開き、以下のように記述。

server: http://localhost:4224
load:
- src/*.js
- test/*.js

コンソールに戻る。さっきサーバーを起動したプロセスウィンドウはそのままにしといて、新しいプロセスを起動。そっちで

$ cd プロジェクトのディレクトリへ
$ java -jar ~/bin/JsTestDriver.jar --tests all

するとテストが走って hoge_test.js に書いたテスト結果が表示される…予定なんだけど、この時点で hoge_test.js には何も書いてないのでエラーになるはず。
というわけで簡単なコードを書いてみる。

### src/hoge.js function jsdriversample() { return true; }
### test/hoge_test.js TestCase(‘jsdriversampletest’, { ‘test should return true’: function () { var result = jsdriversample(); assertTrue(result); } });

で、コンソールでコマンドを叩いてテストする。

$ java -jar ~/bin/JsTestDriver.jar --tests all

ちなみにここで'test should return true'っていうのは各テスト単位の名前で、エラー時にコンソールに出力される。ちなみに文字列だからってマルチバイトは使えない。

assertなんちゃらって関数でテストを実行してるんだけど、使えるassertionの一覧は以下に載ってる。
http://code.google.com/p/js-test-driver/wiki/Assertions
例えば assertTrue だったら、引数の値が true だったらテスト合格になる。

まだちょっと使ってみただけなので、もうちょい勉強して実務に生かしたい。

### 2012-12-06 追記 テストケースのメソッド名(上記の例では 'test should return true' となっているところ)は必ず**test**で始まらないと、認識されないようです。

Mobile Safari、フルスクリーンモード、UIWebView、どれからのアクセスか判別する

apple-mobile-web-app-capableというめmetaタグの値をyesにするとiOS Safariでそのページを「ホーム画面に追加」し、ホーム画面からアクセスした際にページをフルスクリーンモードで開くことができる。

<meta name="apple-mobile-web-app-capable" content="yes" />

出典:Safari HTML Reference – Supported Meta Tags

フルスクリーンモードで開いた場合、navigator.userAgentの値に「Safari」の文字列が現れなくなる。

Moble SafariでのuserAgent(iOS 5.1.1):

userAgent: Mozilla/5.0 (iPhone; CPU iPhone OS 5_1_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9B206 Safari/7534.48.3 

フルスクリーンモードでのuserAgent(iOS 5.1.1):

userAgent: Mozilla/5.0 (iPhone; CPU iPhone OS 5_1_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Mobile/9B206 

ところで、FacebookアプリやTwitterアプリなど、投稿内容のURLを踏むと、そのアプリの中でWebページが展開されるものがある。UIWebViewというやつ。この場合のuserAgentにも、やはり「Safari」の文字列は現れない。

FacebookアプリのuserAgent(iOS 5.1.1):

userAgent: Mozilla/5.0 (iPhone; CPU iPhone OS 5_1_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Mobile/9B206 [FBAN/FBIOS;FBAV/5.1;FBBV/68414;FBDV/iPhone4,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/5.1.1;FBSS/2; FBCR/KDDI;FBID/phone;FBLC/ja_JP] 

TwitterアプリのuserAgent(iOS 5.1.1):

userAgent: Mozilla/5.0 (iPhone; CPU iPhone OS 5_1_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Mobile/9B206 

というわけでSafariという文字列があるかないかを頼りに、フルスクリーンモード、ないしはUIWebViewからのアクセスを判別しようとすると失敗する。 しかし上記出典のSafari HTML Referenceによると、フルスクリーンモードではwindow.navigator.standaloneというプロパティがtrueになるらしいので試した。

デモ

iPhone/iPadのどちらでも、フルスクリーンモードのみでwindow.navigator.standalonetrueになるのを確認できた。

userAgent文字列からiPhone/iPadであることを判定した上で、

  • userAgentにSafariの文字列あり → Mobile Safari
  • userAgentにSafariの文字列なし、かつwindow.navigator.standalone === true → フルスクリーンモード
  • userAgentにSafariの文字列なし、かつwindow.navigator.standalone === false → UIWebView

という判定をすることができる。