immutableJSのMapをredux-persistで保存できないときの対処法

以前説明したimmutableJSは簡単にimmutableオブジェクトを扱うことができるのでreduxのreducerで大活躍します。
normalizr風味のreducerを作る場合は、下記のようにimmutableJSのMapとListで簡潔に書く事ができます。

一つ問題があります。immutableJSはfromJSを利用するとMapオブジェクトでstateを保存します。これが、redux-persistを使うとMapで保存できないという問題です。redux-persistはtransformsという機構でserialize, deserializeを行うことができ、ここにモジュールを適用することで保存できるようになります。redux-persistのauthorであるr2tzz氏が提供するredux-persist-immutableがありますが、READMEの情報が少なくよくわかりません。他のソースを読むと、トップレベルでMapの置換を適用するようです。私の今回のreducerではnavというReact Navigatorのreducerがあるので、ここに適用する必要はないと思っていました。

Redux Persist Transform Immutable

Redux Persist Transform Immutableというものもr2tzz氏が提供しています。これは、reducerごとにシリアライズ・デシリアライズをできるものです。これはREADMEもしっかり書いてあります:)

では、実際のコードを見てみましょう。

使い方は、transformsのvalueにimmutableTransformを入れます。whitelist/blacklistで追加します。どちらかを指定すれば片方がいらないかもしれませんが、わかりやすくするためにwhitelist/blacklist両方いれています。React Nativeでの利用なので、storage: AsyncStorageはそのままです。 ちなみにpersistStore(..).perge()でstateを消去できるので、開発初期にはよく使うことになると思います。

Redux + immutable.js + Reselectでredux reducerを安全かつ簡潔にする

日本語の技術ブログを読んでいるとImmutable.jsでmodelを作り、そこにロジックを入れる方法が書かれてるものが多いですが、英語の情報をまでみてみると、それはどちらかというと特殊な方法で多くのところでReduxのReducerに導入しています。

今回は、後者の方法を説明していきます。

Immutable.js

Reduxのstateはimmutableで操作すべきであり、そのためObject.assign()slice等を駆使してReducerを書いていきます。問題はその方法が冗長であり、人によってES6を使わないなど書き方も違うのでコードが見にくくなります。これを解決するのがImuutable.jsになります。ドキュメントを見ると(Typescript前提で書かれてるドキュメントなので、知らないと非常に読み難い…)、便利な関数が沢山生えてます。setInなどの関数を使えば、Nestされているオブジェクトに一発でデータを挿入できたりします。そして、そのオブジェクトが常にImmutableであることを保証してくれます。

Immutablejsのreducerへの組み込み方は簡単で、InitialStateをimmutableJSで作れるだけです。

上記のstate.draft.standardsはImmutable.Mapオブジェクトとなります。問題はこれをtoJS,toArray, toObjectをコンポーネント内のRedux stateの呼び出しのmapStateToPropsで使うのがReduxのアンチパターンになっていることです。

This is a particular issue if you use toJS() in a wrapped component’s mapStateToProps function, as React-Redux shallowly compares each value in the returned props object. For example, the value referenced by the todos prop returned from mapStateToProps below will always be a different object, and so will fail a shallow equality check.

で、どうするか。ここで登場するのがReselectになります。

Reselect

Reselectは、キャッシュを持ち、変更があった場合だけ新しいオブジェクトを作成するような挙動を記述できます。これをmapStateToPropsに入れ込むことにより、アンチパターンを避けることができます。 具体的なコードは、

詳しくはREADMEや他の技術ブログを参考にしていただければと思いますが、標準の比較関数は単純なオブジェクト比較(===)を利用しているため、オブジェクトのkey/valueのペアまで比較できません。ここでlodashisEqualを使って、deep比較をすることにしている(今回の場合、shallowな比較(ネストされたものは比較しない)で十分ではあるがREADMEがそうなっているので))。createDeepEqualSelectorの最初の引数が、Input Selectorと呼ばれるもので、ここが以前のもの(キャッシュ)との比較対象です。isEqualコードを読んでいないが直感的にあまりnestしすぎない方が比較自体の計算量が少なくなって良さそうですね。

これをcomponentのmapStateToPropsで直に呼び出すのは、単純です。

これでreducer内でimmutablejsで快適にstateを操作できるようになりました!

[React Native] HTTP requestのRetryを実装する

Mediumに英語のエントリRetry POST request when it’s failed on React Nativeを書いた内容と同じですが、地下鉄で地下に潜るとreact-native-oauthmakeRequestが失敗するケースが発生しました。Twitterの2つ目の投稿が投げられていなかったり、FBへの投稿も落ちていました。そこで、リトライするコードを書きました。

Async Retryを使います。これはnode-retryをラップしたもので、例がシンプルで良さそうだったので使います。(async/awaitの文法にも慣れているので)

問題なくインストールできます。そして、コードは、下記のような感じ。

2つ目の引数にnode-retryのオプションを書いてくイメージです。今回はエラーハンドリングは特にしないで5回リトライする単純なコードです。iphoneのデベロッパーのところにある”Very bad network”で試したところ、コンソールに数回トライしているのを確認しました。(Testをうまく書けるのかな。。)

以上、リトライを実装したい方は参考にしてみてください。

[React Native]日本語環境下で日本語が垂直のセンターを取れない問題(vertical-align: middle)の対処法

地味に辛いこの問題。vertical-align: middle問題。 iOSが英語環境(設定 -> 一般 -> 言語と地域が英語)の場合は問題なくセンターとれるのですが、日本語環境にするとflex-startみたいにpaddingTopがゼロになってしまう問題があります。やっかいなのは、paddingで調整するとAndroidはこの問題がないため表示がずれます。。

コードはボタンだったら、下記みたいな感じ。ボタンに限らず、この問題はおきていると思います。

解決方法は、fontサイズと同じViewでラップする。(試行錯誤後、閃きました)

[React Native] Codepushを導入してみて

Codepush

Mircosoftが提供するプラットフォームで、CordovaとReactNative用のバイナリを変えずに該当コードだけを変更するものです。React Nativeの場合は、JSバンドルだけをダウンロードして差し替えることができます。何がうれしいのかというとレビューを通さずにコードを変更*1することです。とはいえ、最近は3日以内にAppleのレビューが終わりますし、そんなに必要ではない仕組みかもしれません。

実際導入してみると、運用のオーバーヘッドは大きめです。数人で開発・運用しているアプリなら問題ないと思いますが、個人プロジェクトだとメンテが大変です。そして、気づくことはitunes connectやGoogle playでの変更は必要ということです。イメージ的に大きめの変更はアプリレビューを通して、小さなもの、もしくは致命的なバグはCodepushしてくのが実際の使いどころになりそうだと感じています。

*1 アプリの意図を変えないようにしましょう。

では、さっそくインストールしましょう。

インストール

README通りです。

CLIインストール

Deployment keyが表示される。
READMEとは前後してしまいますが、下記のようなことが後で言われるので, iosとandroid両方作りたい場合は、両方のアプリを作っておきます。

Note, if you are targeting both platforms it is recommended to create separate CodePush applications for each platform.

アプリ側の設定

Githubに飛んでReadme通りに設定を進める。注意点としては、READMEがReact Nativeのバージョンごとに違うこと。 自分が使ってるReactNativeに相当するブランチをTagから選択。(ということは、将来的にRNをアップグレードしたらCodePushも変更しなくてはいけない。。)

このあとios/androidの設定ファイル(xcode, gradle)の変更する。ここは各READMEを参照のこと。

アプリにcodeプッシュを適応するのは簡単で、単純にWrapするだけ。
Reduxを利用してるので少しReadmeとは違うが、単純にRegisterComponentの前にラップするだけ問題ない。

またcodePushOptionsでオプションを指定できる。上記は、悩んだ末に決めたバックグランドからの復帰時にアプリのバイナリを確認・そして次期バージョンがあったら、次のバックグランドからの復帰時にインストールする。

すぐにインストールする等もできるが、バスンとアプリが落ちて起動する感じになるので、ユーザは気分がよくないでしょう。。

アプリのリリース方法

これでStaging環境にアップロードされる。
ベータ版を端末に入れておいて、テストしてみて、オッケーなら本番環境にあげる。

開発環境

README通りにやると下記のようになる。

iOS

xcode project schema

  • debug: ローカルでシミュレーターを利用して開発する。code pushは利用しない。
  • staging: SchemaをStagingに変更し、xcodeからデバイスへ転送。iPhoneにStagingバイナリを転送済の場合、USBなしで実機テストができる。
  • production: SchemaをStagingに変更し、xcodeからデバイスへ転送。または、アーカイブしてitunes connectにアップロード。エンドユーザが使う。iTunes storeから配布されるアプリが参照する。

Android

  • debug: react-native run-androidでemulatorでテストする。
  • releaseStaging: react-native run-android --variant=releaseStaging
  • release: cd android && ./gradlew assembleRelease でAPKをGoogle Playにアップロード。

android/app/build.gradleは下記のようになるはず。

Deployment状況の確認

AndroidのProduction環境のバイナリの作り方

リリース方法はiosと同じ

Troubleshooting

基本的に難しい。迷った場合は、下記を確認する。

  • Versionが正しくない
  • オプションのパラメータが正しくない
  • 同じバージョンのバイナリだとプッシュされない(?)

デフォルトパラメータで設定する場合、Pushしたときに、そのオプションが次のcodepushの振る舞いになるので。現在codepush側のパラメータが何であるか覚えておかないといけません。

まとめ

ただでさえ、React Nativeのバージョンに追いつくのに必死なのに、Codepushのバージョンも考えないといけないのはつらい。起動時にcodepush側に最新があるか見にいく仕様なので、app storeで配布しているバイナリに欠陥があってはいけない。例えば、Sign UpのViewにバグをcodepushでは直しにくい。なぜなら、Resumeの場合、バックグランドから戻るタイミングがないし、再起動を強制するのは、(ダウンロードする時間があるので)サインアップの途中で再起動してしまう可能性がある。またApplinkでFacebookログインを外部に行く場合は、そこでResumeが起こってしまうのも注意。つまり、Facebook認証後戻ってくるときにアプリが再起動してしまう。

やはり、使い所はApp storeのレビューが終わるまで直せないような致命的なバグ。赤い画面で落ちてしまうようなバグはリスタートが必要なので、緊急パッチを当てることができる。またStaging環境の実機テストもパッケージをアップロードしなくていいので楽になる。

codepushをセットアップしても必ず使う必要はない。release等がなくても元のバイナリでユーザは使えることができる。なので、余力のあるチームは、導入を検討してみてはどうでしょうか?

[React Native] Androidのパッケージ名の変更方法

react-nativeコマンドで、アプリを作るとkagami.comという名前空間を自動生成するが、これをkagami.wazalab.comというようなXcodeのようにもう一段階ネストしたい。
下記の3ステップを踏めばよい。

1. ディレクトリ変更

2. ファイル内のパッケージ名変更

com.kagamiからcom.wazalab.kagamiに変更する。

  • android/app/src/main/java/com/wazalab/kagami/MainActivity.java
  • android/app/src/main/AndroidManifest.xml
  • android/app/build.gradle

3. 再生成

起動しようとすると、下のようなエラーがでる。deeplinkが動かないみたなことがあるらしいが、これは基本、自動でアプリが起動しないだけ。エミュレータにいってタップして起動させる。
(githubにissueが上がっているが、PRがマージされてない様子…)

[React Native] Androidアプリのバージョンの指定方法

Androidは開発したことないのでわからないことが多い。 
android/app/build.gradleversionCodeversionNameを変更すればいい。

ちなみにこの変数は、

  • versionName - XcodeのVersionと同じ。ただじ数字でなくてもよい。
  • versionCode – XcodeのBuildと同じ。ビルドごとに変更しないとpushできない。

参考: http://stackoverflow.com/questions/35924721/how-to-update-version-number-of-react-native-app

[React Native] AnalyzerでBUILD FAILEDな時の対処法

Projectのディレクトリを移動されたらreact-native run-iosで失敗するようになった。

キャッシュを使用してBuildを試みてて失敗しているようなので、ビルドを消す。

React Native Router Fluxでタブの履歴を残す方法

InstagramのようにAndoridのバックボタンで前のタブに戻るようにするためにどのタブが前のタブか知る必要があります。React Native Router Flux(with Redux)を使っていて、これを実現するために下記のようなReducerを作りました。

やってることは、FOCUSしたときにtabHistoryとして、Arrayに追加してるだけです。無限に履歴を残す必要はないので10個で制限してますが、もしかしたら2つでいいかもしれません(最初の要素は現在のタブになるので二つ以上は必要)

で、これを下記みたいな感じでBackAndroidで使用すれば良いでしょう。

Scroll to top