ReactNativeでの開発を通じて得た知見

前回はてぶのお気に入りフィードを読むHBFavというアプリのReactNative版RNHBFavというアプリを作っているという話を書いたが、とりあえずAppStoreへ申請するところまで終わった。

razokulover.hateblo.jp

申請がどのくらいで通るかはまだわからないが、たぶん1週間はかかる気がする。

少し時間が空きそうだし、ここらで今回ReactNativeで開発〜リリース申請する中で感じたことやこうした方が良かったみたいなものをメモしておこうと思う。

垂直分割/水平分割のディレクトリ構成

ディレクトリ構成はプロジェクトごとにみなそれぞれ自分なりの構成を持っているようだけど、例えばreduxを利用するアプリだと以下のような作りになると思う。

index.ios.js
index.android.js
src
|__actions
   |__hoge.js
|__reducers
   |__index.js
   |__hogeReducer.js
|__containers
   |__top.js
   |__detail.js
|__components
   |__index.js
   |__list.js
   |__buttonA.js
   |__buttonB.js
|__constants
   |__hoge.js
|__assets // stylesheetや画像とか
|__configureStore.js
|__App.js

これはいわゆる役割ごとにまとめる垂直分割という構成。MVCとかそういったパターンを使ったフレームワークでは馴染みのある構成。

一方でページごとにまとめる水平分割という構成もある。例えばこんな感じ。

index.ios.js
index.android.js
src
|__components
|__containers
   |__TopPage
      |__index.js
      |__action.js
      |__reducer.js
      |__style.js
      |__list.js
      |__buttonA.js
   |__DetailPage
      |__index.js
      |__action.js
      |__reducer.js
      |__style.js
      |__list.js
      |__buttonB.js
|__configureStore.js
|__App.js

actionやreducerなどをページ配下のディレクトリに全部突っ込む。

垂直分割だと、ファイルが散りまくっていくので大規模な構成になった時に管理しずらくなる印象。

水平分割だと、ファイルがページごとのディレクト配下に全てまとまってくので管理が楽。

趣味の範囲かもしれないが、ある程度のページ数のアプリになることが想定されるのならば水平分割の構成も選択の余地がある。

RNHBFavの場合は、reduxを利用しないで作る+そこまでページ数が増えないだろうという想定で垂直分割ではじめたが、徐々にページが沢山必要なことがわかる&Redux使わざるをえないみたいな状況になりやや煩雑な構成になりつつある。

できるだけiOS/Androidで使えるコンポネントやライブラリを選ぶ

react-native-OOOのようなライブラリが沢山あるができるだけiOS/Androidで利用できるものを使うべき。

片方しか対応していない場合、最悪iOS/Android対応が詰むし、代替するとしても自分でネイティブコードを触ったり、コード内に分岐が沢山発生してしまう。

NativeBaseReact Native ElementsなどコンポーネントのBootstrapのようなライブラリがあるが、これらはオリジナルのUIを作らないのであればiOS/Android対応もしているので便利。

ただし、オリジナルのUIを作る必要があるなら下手に依存を増やしてしまうことになるので標準コンポーネントを利用したほうがよい。

コンポネントを汎用化したい欲を抑える

ヘッダーとかボタンとかある程度は汎用的に使えるコンポーネントもあるが、案外ページごとに微妙に使い方が違ってきたりして頓挫しがち。

最初のうちはページごとに作ってみて、3回くらい同じコンポーネントを書き始めたら別ファイルに分けるくらいの気持ちで。

ReactNativeはプロトタイプとかをサクッと作ることが求められてることも多いと思うし、綺麗さよりある程度雑にサッと作るくらいで良い気がする。

reduxはページをまたいで情報を共有しないといけなくなってからはじめて導入を考えるべき

Reduxをいつ/どのように導入するかはwebのほうでも大変悩ましい問題ではあるが、アプリの場合はページをまたいで情報を共有しないといけなくなってからはじめて導入すべきと考えている。

RNHBFavの場合であれば、メニューページでのお気に入りor自分のブックマークのクリックアクションに紐付いて取得されたブックマーク情報が、TOPページのタイムラインに即反映させるようにしたいといった時。

メニューページとTOPページで情報元が共通化されていないといけない、これがページをまたいで情報を共有しないといけない状態。

それ以外の場合は、stateで管理して、親から子コンポーネントにパスしていくやり方でいいと思う。

色んなファイルにあれこれ書いたりしなくていいし、単純に理解しやすいので。

以前作った某GIFビューアーではページごとにReducerを作って管理しようとしてしまって大失敗した。 この時は結局ほぼ書き直しでReducerが10ファイルくらいから1ファイルにまで減った。

非同期処理

今回は外部APIから情報を取得して完了したらaction発行くらいしかなかったのでredux-promiseを使った。

場合によってはredux-thunk、redux-promise, redux-sagaあたりを検討しないといけない。

個人的には非同期処理を連ねまくりたいときはredux-sagaを使う。

このエントリが詳しい↓

redux-sagaで非同期処理と戦う

ルーティングは込み入ったことしないならreact-native-router-fluxで十分

Modal,Drawer,UINavigation的画面遷移でよいならこれで問題になったことない。

RNHBFavではこんな感じ。

import React from 'react';
import { Router, Scene } from 'react-native-router-flux';
import Root from './components/RootScreen/index';
import Bookmark from './components/BookmarkScreen/index';
import BookmarkComment from './components/BookmarkCommentScreen/index';
import BookmarkEdit from './components/BookmarkEditScreen/index';
import Entry from './components/EntryScreen/index';
import Auth from './components/AuthScreen/index';
import Menu from './components/MenuScreen/index';
import Tour from './components/TourScreen/index';
import Eula from './components/EulaScreen/index';
import UserBookmark from './components/UserBookmarkScreen/index';
import BookmarkStar from './components/BookmarkStarScreen/index';

const App = () => (
  <Router>
    <Scene modal>
      <Scene key="root" hideNavBar>

        {/* TOPタイムライン */}
        <Scene key="home" component={Root} initial />

        {/* ブックマーク詳細 */}
        <Scene key="bookmark" component={Bookmark} />

        {/* Webviewでエントリを開くページ */}
        <Scene key="entry" component={Entry} />

        {/* ユーザーのブックマーク一覧を見るページ */}
        <Scene key="userBookmark" component={UserBookmark} />

        {/* ブコメ一覧 */}
        <Scene key="bookmarkComment" component={BookmarkComment} hideNavBar panHandlers={null} />

        {/* ブコメのスター付けてる人一覧 */}
        <Scene key="bookmarkStar" component={BookmarkStar} hideNavBar panHandlers={null} />
      </Scene>

      {/* メニュー */}
      <Scene key="menu" component={Menu} hideNavBar panHandlers={null} />

      {/* 初回のみツアーページ */}
      <Scene key="tour" component={Tour} hideNavBar panHandlers={null} />

      {/* 利用規約 */}
      <Scene key="eula" component={Eula} hideNavBar panHandlers={null} />

      {/* 認証ページ */}
      <Scene key="auth" component={Auth} hideNavBar panHandlers={null} />

      {/* ブコメ投稿フォーム */}
      <Scene key="bookmarkEdit" component={BookmarkEdit} hideNavBar panHandlers={null} />

      {/* ModalでWebview開きたい時用 */}
      <Scene key="modalEntry" component={Entry} hideNavBar panHandlers={null} />
    </Scene>
  </Router>
);

export default App;

iOSでは標準のWebviewは使わない

標準のWebviewはiOSにおいてはCustom URL Schemeに対応してないとかSP対応してないページ(SpeakerDeckとか)の表示レイアウトが崩れるなど問題が多発する。

これはWKWebviewではなくUIWebViewを使ってるので発生する問題で、ReactNative側の問題。だが、あまり修正する気がなさそうなのでreact-native-wkwebview-rebornを使ったら良い。

ただしiOSしか対応してないのでAndroidでは標準のWebviewを使うしか無い。

タイムライン系のUIはFlatList使う

タイムラインのようなUIを作る時はListViewではなくFlatListを使ったほうが良い。

FlatListだと数百アイテムの作成でもサクサク動く。

ただし、keyExtractorなどプロパティをちゃんと設定しないとリストが更新されなかったりするので詳しくはちゃんとドキュメント読んで。

react-native-vector-icons便利

ほとんどのアイコンはここで調達できる。 react-native-vector-icons

一つ注意したいのは、アイコンは沢山種類があるがどのIconグループを利用するかは統一したほうが良い。

自分は種類の多さからMaterialCommunityIconsを利用した。

styleはインライン使わない

webと同様インラインスタイルはコードが読みづらくもなるし、やめたほうがよい。

色やサイズは定数化して汎用的にしたほうがよい

アプリ全体の統一感を出すためにも色は、ベース・ナビゲーション・リンク・テキスト・サブテキストくらいは定数化しておくと便利。 フォントサイズも同様。

通信処理はfetch apiで十分

axiosとか使おうとも思ったが標準で使えるfetch apiで十分。無駄に依存を増やさ無いほうが良い。

凝ったアニメーションエフェクトはできないと思え

徐々に透過になったりふわっと表示されるようなああいうやつ。 ああいうのをごりごりやりたい場合はネイティブでやったほうがよい

それでもって場合はこのへん読むと。

ESLint使う

ReactNativeに限らないがESLint入れてコードに統一感出しといたほうがあとあと楽。

よく読んだReactNativeで公開されてるソースコード

良さげなコンポーネントを調べるならjs.coachのreact-nativeのカテゴリ

https://js.coach/react-native

iOSでリリースするための準備は少し癖がある

リリース用にmain.jsbundleを作成する必要があるので忘れず。

react-native bundle --dev false --assets-dest ./ios --entry-file index.ios.js --platform ios --bundle-output ios/main.jsbundle

あと画像とかは、Build Phases > Copy Bundle Resourecsに登録されてるか確認しないとリリース版でのみ画像が表示されないみたいな状況に陥る。

困ったら#reactnativeでつぶやく

最後に

結構まとまりなく書きなぐってしまった。

ディレクトリ構成とかコンポーネントに対する箇所はだいぶ異論あると思うけど、個人的にはこうやってますということで。

ちなみにRNHBFavはソースも公開してるのでよかったら参考に。

以上。

参考リンク