ハウテレビジョン開発者ブログ

『外資就活ドットコム』を日夜開発している技術陣がプログラミングネタ・業務改善ネタ・よしなしごとについて記していきます。

React Nativeにおける多タブかつ件数の多いリストをつつがなく表示させるには

TL; DR

  • はじめに
  • 主要なライブラリ/ディレクトリ構成
  • このようなリストのおはなしです
  • 実装上で気をつけたポイント
  • 起こりがちな問題点 NINJA
  • NINJAを防ぐにはどうするか
  • おわりに

はじめに

こんにちは!世界で挑戦したいと思う学生に向けた就活支援プラットフォーム、外資就活ドットコムの開発チームです。 先日iPhoneアプリをReact Nativeでリニューアルの上リリースしました!(パチパチ)

さて、このアプリは募集情報や体験記、コミュニティなど、比較的件数が多いリストを表示させる箇所がいくつかあります。

そしてそういったリストも業界などのカテゴリやタグでのタブ切り替え表示やソート順などでの切り替え表示をさせています。

つまり、

- データの管理
- 大量のコンポーネント(->ここではリストのアイテム1個1個のこと)の表示管理
- 遅延の少ない切り替え

といったことが必要になってきます。

それを実現するまでにどのような構成で作っていったか、また想定外のハードルも乗り越える必要があったのでそこで得た知見をシェアできればと思います。

主要なライブラリ/ディレクトリ構成

本題に入る前に主要なライブラリ構成を書いておきます。(一部抜粋)

redux:状態を一元管理
react-redux:reactとreduxをつなげる
redux-saga:非同期処理
redux-persist:データ永続化
react-native-router-flux:画面遷移 (使い方はreact-routerに似ている)
styled-components:セマンティックにCSS設計ができる
NativeBase:UIコンポーネント補助

またこのようなディレクトリ構成になっています。

src:
  actions:reduxのaction
  api:sagaで呼び出すapi
  components:各画面で呼び出すコンポーネント (部品)
  constants:定数定義(reduxのaction名他)
  containers:画面(reduxとconnect)
  reducers:reduxのreducer
  router:react-native-router-fluxのrouter
  saga:redux-sagaのsaga

*本エントリでは各項目の詳しい内容についてはレクチャーしないので、参考リンクを置いておきます

このようなリストのおはなしです

下記画像のように業界タブごとに表示させることも可能で、 企業名や体験記の種別で絞り込んだり、人気順などでソートすることも出来ます。

f:id:tacker-howte:20180629171626p:plain

実装上で気をつけているポイント

さて、つらつらと書いていくと長くなってしまうので、気をつけたポイントを列挙していこうと思います。

データの管理

リストの内容とフォロー状態などのstateは分ける

リスト取得時にフォロー状態も合わせて取得できるようなAPIにしていますが、それをreduxのstateに保管するときは内容とフォロー状態(KeyValue)、またページング(KeyValue)に分けて保管しています。

そうすることでフォローのコンポーネントがそのリストアイテム以外にある場合でも瞬時に状態を共有でき、変わった場合それぞれのコンポーネントで再レンダリングを走らせることが可能になります。

また下記のように、内容とページングはカテゴリ(タブ)ごと、またソート順ごとに保管するためそこまではObject(連想配列)で保管するのに対し、フォロー状態はアイテムのKeyValueなので単純な配列で保管しています。

reducers/reportListReducer.js

  const initialState = {
    ...
    actionPage: {},
    reportListByIndustryCategory: {},
    reportClips: [],
    ...
  }

ソート順を保管し、各ソート順のページ状態も保管する

先ほどソート順に保管すると書きましたが、 これはそうすることで単純なソート切り替え時に(すでに取得済みの場合)通信が走らずにパッと切り替えられるようにすることを目指しました。

またページングも合わせて保管することでエッジケースの齟齬がないようにしています。

(ここはSagaを利用してバックグラウンドで表示しているタブ以外も逐次取得したいと思っているのですが、フィルタ切り替えを頻繁に行うなどの際に今の所うまくいっていないのでちょっと放置しています)

reducers/reportListReducer.js

  case REPORT_LIST_GET_LIST_SUCCESS: {
      const lists = deepCloneObj(state.reportListByIndustryCategory);
      const actionPage = deepCloneObj(state.actionPage);
      ...
      actionPage[`${action.industryCategoryId}`][`${action.order}`] = action.page;
      ...
      lists[`${action.industryCategoryId}`][`${action.order}`] = lists[`${action.industryCategoryId}`][`${action.order}`].concat(action.reports);

      const reportClips = state.reportClips.slice();
      action.reports.forEach((val) => {
        reportClips[val.id] = { clipped: val.clipped, count: val.clipsCount };
      });

      return {
        ...
        reportListByIndustryCategory: lists,
        ...
        actionPage,
        reportClips,
      };
    }

大量のコンポーネントの表示管理

React Nativeではいかに再レンダリングを走らせないようにし、端末に負担をかけさせないようにするかが重要になります。

そのためコンポーネントは(ユーザーがフォローを行うなど)頻繁に表示が変わりうるものとそうでないものは分けています。 表示が変わらないものは shouldComponentUpdate=false or PureComponent 使用をしています。

また、リストのコンポーネントはVirtualizedList を使用しているのですが、Props removeClippedSubviewstrue にすることでメモリ使用量の増大や突然アプリが落ちる事態を防いでいます。

遅延の少ない切り替え

  • react-native-scrollable-tab-viewprerenderingSiblingsNumber1にする

    こちら、NativeBaseのScrollableTabを使用すると実質的にはこのライブラリを使うことになるのですが、 この prerenderingSiblingsNumber に1を設定しないと タブ切り替え時の挙動がモタっとしてしまいます。

React NativeではちょっとしたPropsの設定で挙動のタイミングが大幅に変わり、 体験も大きく変わってしまうことがよくあるので気をつけましょう。

  • react-native-scrollable-tab-view の onChangeTab で変更したカテゴリ情報を componentWillReceiveProps で検知する

    まだリストを取得していないタブに移動する場合、当初は onChangeTab でリスト取得処理も合わせて行なっていたのですが、そうすると描画までもたついてしまうんですね。

    そこでここでは単純なタブ移動(ここではつまりカテゴリの変更)のみreduxに通知、保管し、それを reactのlifeCycleである componentWillReceiveProps で 状態の変更を検知し、リスト取得処理を走らせるようにしました。 これによりレンダリングはそこまで遅延せずに済むようになっています。

    • ちなみにUpdate on Async Rendering にあるように、 componentWillReceiveProps は React 17以降削除されてしまうという移り変わりの早さが良くも悪くもすごいですね。

※なおデータの管理で書いた

- ソート順を保管し、各ソート順のページ状態も保管する

も遅延の少ない切り替えに貢献しています。

起こりがちな問題点 NINJA

上で書いた実装上の気をつけるポイントをしなくても、社内でNINJAと呼ばれているレンダリングの不正が起こります。

開発スタートした当初はリストで11番目のアイテムを表示させようとしただけでリスト全体がぐしゃっと重なった表示(隠れ身の術)になるなど、常にリスト周りは暗雲が立ち込めていました。。

また、タブもうまくデータ管理しておかないとタブが左右に高速回転しだす(分身の術)など、憎めないヤツです(笑) f:id:tacker-howte:20180629175313g:plain

一番悩まされたのは下部メニューを切り替えるとたまーに真っ白になるケースでした。これはとあるウルトラCを使っているのですが、気になる方はぜひオフィスに遊びに来てください♪

NINJAを防ぐにはどうするか

一番疑った方が良いのは、意図せず複数のAPI取得が走っていないか、またニアリーイコールですが意図せぬ挙動が走っていないかです。

なので action がいつ走っているか常にデバックしないとですね。

そしてそれでもダメな場合はreactやredux、redux-saga公式のdocsとにらめっこしましょう。ほとんどの場合はなんとかなります。(たぶん)

終わりに

最近 React Native at Airbnb: The Technology が話題ですが、 現在の弊社アプリの規模感だと中期的にはReact Nativeをこれからも使用していく予定です。(もっと大規模になるとAirbnbのように別の形をトライするかもしれませんが。)

ただ、React Nativeにおいても既存のiPhone/Android開発の知見は必要な部分も多いので、そういう方で新しい技術にもトライしてみたい方など絶賛募集中です。

またReactとかVueとかAngularとか触ってはいるけどまだアプリに手を出していない方も絶賛募集中です!

ぜひぜひオフィスに遊びに来てくださいー!

ご連絡はこちらよりお待ちしております。