React Native の画面遷移・管理をオープンソースのアプリを見ながら学ぶ

最近趣味のアプリ開発で React Native を使っています。いつもは [React Redux を使った SPA の Web アプリ](https://engineer.blog.lancers.jp/2017/11/react-redux-spa-manage-tips/) とその Cordova アプリを書いています。
Web アプリだと、画面は react-router で管理するのが定番ですが、 React Native の場合は、 react-router を使わず他の手法で実装している様なので、オープンソースのアプリで使われている実装など、調べて学んだ結果をご紹介したいと思います。

## 参考にしたアプリ一覧
[React-Native-Apps](https://github.com/ReactNativeNews/React-Native-Apps) に React Native で作られているオープンソースのアプリケーションがいろいろ公開されているので、参考にしてみました。

アプリ名|説明
---|---
[git-point](https://github.com/gitpoint/git-point)| github を見るアプリ。割とサクサク動くのでコード読むのにもオススメ
[gas-oil-mixture-mobile](https://github.com/filippofilip95/gas-oil-mixture-mobile)|オイルの計算機
[HackerBuzz-ReactNative](https://github.com/RCiesielczuk/HackerBuzz-ReactNative)|ハッカーニュース的なアプリ
[f8](https://github.com/fbsamples/f8app)|facebook のカンファレンスのアプリ


## シンプルなスワイプの遷移
[git-point](https://github.com/gitpoint/git-point) で使われていた、スワイプの遷移。
![racord5.gif](https://qiita-image-store.s3.amazonaws.com/0/26095/ee7b83c7-40d0-115b-1012-16c28cf0c2e0.gif)

[git-point/blob/master/src/auth/screens/login.screen.js](https://github.com/gitpoint/git-point/blob/master/src/auth/screens/login.screen.js) に全体の実装があります。[react-native-swiper](https://github.com/leecade/react-native-swiper)　というスワイプのライブラリを利用しているようです。
使い方だけもとてもシンプル。 `Swiper` で `View` を囲うだけ。(ライブラリ本家の方のサンプルです)

```javascript:swiper-sample.js
import Swiper from 'react-native-swiper';
import { Text, View } from 'react-native';

...

return (
  <Swiper
    showsButtons={false}
    dotColor="#FFFFFF55"
    activeDotColor="#FFFFFFFF"
  >
    <View>
      <Text>Hello Swiper</Text>
    </View>
    <View>
      <Text>Beautiful</Text>
    </View>
    <View>
      <Text>And simple</Text>
    </View>
  </Swiper>
)
```


## 戻るボタン付き遷移
[gas-oil-mixture-mobile](https://github.com/filippofilip95/gas-oil-mixture-mobile) にて実装されていたシンプルな戻るボタン付き遷移。
![recortde3.gif](https://qiita-image-store.s3.amazonaws.com/0/26095/d0fb0895-0d26-e1b2-ee94-b53274962a3a.gif)


[react-navigation の StackNavigator](https://reactnavigation.org/docs/navigators/stack) を使っています。StackNavigator で遷移させると、デフォルトで`前に戻るボタン`が遷移先の左上に現れます。

このアプリの動作では、右上ボタンのクリックでオプションページに遷移します。（縦三点のボタンでアクションリストが開くのではなく、画面遷移なのはちょっと変な気もしますが。。） [gas-oil-mixture-mobile/blob/master/src/router.js](https://github.com/filippofilip95/gas-oil-mixture-mobile/blob/master/src/router.js) にある通り、実装自体もシンプルです。(以下一部抜粋）画面遷移とは関係でないですが title なども I18n で多言語対応しているところが素敵ですね。

```js:router.js
import { StackNavigator } from 'react-navigation';

import Calculator from './screens/CalculatorHoc';
import Options from './screens/OptionsHoc';
import { OptionsButton } from './components';
...

const Stack = StackNavigator(
  {
    Calculator: {
      screen: Calculator,
      navigationOptions: ({ navigation }) => ({
        title: I18n.t('oilRatioCalculator'),
        headerRight: (
          <OptionsButton onPress={() => navigation.navigate('Options')} />
        ),
      }),
    },
    Options: {
      screen: Options,
      navigationOptions: () => ({
        title: I18n.t('options'),
      }),
    },
  }
);

```

画面の管理も遷移数が少ないので、`router.js` に全部の状態をもたせて　`/screens/xxx` から、各ページのデータを取ってくる構成なのでシンプルですね。

## タブ遷移
[HackerBuzz-ReactNative](https://github.com/RCiesielczuk/HackerBuzz-ReactNative)
 にて実装されているタブの遷移です。上部のタブ遷移と、下部のタブ遷移の二つがあります。


![recortde3.gif](https://qiita-image-store.s3.amazonaws.com/0/26095/5d76a3c6-33c3-8dfc-2516-717d9b8bb10f.gif)

### 上部のタブ遷移
上部のタブは [react-native-scrollable-tab-view](https://github.com/skv-headless/react-native-scrollable-tab-view) で実現されています。タブクリックで、画面がスワイプします。
[RCiesielczuk/HackerBuzz-ReactNative/blob/master/js/containers/main/StoriesContainer.js](https://github.com/RCiesielczuk/HackerBuzz-ReactNative/blob/master/js/containers/main/StoriesContainer.js) より、一部抜粋。

```js:StoriesContainer.js
import ScrollableTabView from 'react-native-scrollable-tab-view';

export default class StoriesContainer extends Component {
  render() {
    return (
      <ScrollableTabView>
       {/* タブコンテンツ１ */}
       {/* タブコンテンツ２ */}
       ...
      </ScrollableTabView>
    )
  }
}

```

### 下部のタブ遷移
次に、下部のタブ遷移です。このアプリも react-navigation を使っていますが、[addNavigationHelpers](https://reactnavigation.org/docs/navigators/custom#addNavigationHelpers) という機能を使って画面遷移を実装しています。

[HackerBuzz-ReactNative/blob/master/js/containers/Router.js](https://github.com/RCiesielczuk/HackerBuzz-ReactNative/blob/master/js/containers/Router.js) の実装を一部抜粋


```js:Router.js
...
import { addNavigationHelpers } from 'react-navigation';
import { TabBar } from './navigationConfiguration';
...

class Router extends Component {
  render() {
    const { dispatch, navigationState } = this.props;
    return (
      <View style={{ flex: 1 }}>
        <TabBar
          navigation={addNavigationHelpers({
            dispatch: dispatch,
            state: navigationState
          })}
        />
     </View>
    );
  }
}
const mapStateToProps = state => {
  return {
    navigationState: state.tabBar,
    storiesState: state.storyList
  };
};

export default connect(mapStateToProps)(Router);
```
Redux の Container Component(redux の state を受け取る層) として、Router を定義しています。どのページにいるのかを状態と捉え、 Redux の state として扱っているようです。 また `<TabBar>` というコンポーネント自体は `navigationConfiguration.js` のファイルで定義しており、その中で [react-navigation の TabNavigator](https://reactnavigation.org/docs/navigators/tab) を利用して、複数のタブに遷移できるようになっています。


```js:navigationConfiguration.js
import { TabNavigator } from 'react-navigation';
import TabMainNavigation from './main/TabMainNavigation';
import TabAskNavigation from './ask/TabAskNavigation';
...

const routeConfiguration = {
  TabMainNavigation: { screen: TabMainNavigation },
  TabAskNavigation: { screen: TabAskNavigation },
  ...
};

const tabBarConfiguration = {
  tabBarOptions: {
    showIcon: true,
    tabBarPosition: 'bottom',
    ...
  }
};

export const TabBar = TabNavigator(routeConfiguration, tabBarConfiguration);
```

`TabMainNavigation `, `TabAskNavigation` など、タブごとの Container Component を作成して、それぞれその中で、StackNavigator などを呼び出して、複合的な画面遷移を実現しています。


[HackerBuzz-ReactNative/blob/master/js/containers/ask/TabAskNavigation.js](https://github.com/RCiesielczuk/HackerBuzz-ReactNative/blob/master/js/containers/ask/TabAskNavigation.js)

```js:TabAskNavigation.js

import React, { Component } from 'react';
import { addNavigationHelpers } from 'react-navigation';
import { NavigatorTabAsk } from './navigationConfiguration';
import { connect } from 'react-redux';
import Icon from 'react-native-vector-icons/FontAwesome';

class TabAskNavigation extends Component {
  static navigationOptions = {
    tabBarLabel: 'Ask',
    tabBarIcon: ({ tintColor }) => (
      <Icon size={20} name={'question'} color={tintColor} />
    )
  };

  render() {
    const { dispatch, navigationState } = this.props;
    return (
      <NavigatorTabAsk
        navigation={addNavigationHelpers({
          dispatch: dispatch,
          state: navigationState
        })}
      />
    );
  }
}

const mapStateToProps = state => {
  return {
    navigationState: state.tabAsk
  };
};

export default connect(mapStateToProps)(TabAskNavigation);
```

[HackerBuzz-ReactNative/blob/master/js/containers/ask/navigationConfiguration.js](https://github.com/RCiesielczuk/HackerBuzz-ReactNative/blob/master/js/containers/ask/navigationConfiguration.js)
タブ遷移した画面からさらに `StackNavigator` を呼び出して、戻るボタン付き遷移をできるようにもしているようです。

```js:navigationConfiguration.js 
import { StackNavigator } from 'react-navigation';
import AskStoriesContainer from './AskStoriesContainer';

const routeConfiguration = {
  AskStoriesContainer: { screen: AskStoriesContainer },
};

const stackNavigatorConfiguration = {
  initialRoute: 'AskStoriesContainer',
  navigationOptions: {
    headerBackTitle: null
  },
  gesturesEnabled: true,
};

export const NavigatorTabAsk = StackNavigator(
  routeConfiguration,
  stackNavigatorConfiguration
);
```

[HackerBuzz-ReactNative/blob/master/js/reducers/index.js](https://github.com/RCiesielczuk/HackerBuzz-ReactNative/blob/master/js/reducers/index.js)

画面のそれぞれの状態管理に関しては reducer 側で [getStateForAction](https://reactnavigation.org/docs/routers/api#getStateForAction-action-state) を利用することで、指定したアクションに応じてナビゲーションの状態を決めることができます。（正直自分もこの遷移方法を完全に理解できてないです。）

```js:reducers/index.js
import { combineReducers } from 'redux';

import { NavigatorTabMain } from '../containers/main/navigationConfiguration';
import { NavigatorTabAsk } from '../containers/ask/navigationConfiguration';

import { tabBarReducer } from '../containers/navigationConfiguration';

const rootReducer = combineReducers({
  tabBar: tabBarReducer,
  tabMain: (state, action) =>
    NavigatorTabMain.router.getStateForAction(action, state),
  tabAsk: (state, action) =>
    NavigatorTabAsk.router.getStateForAction(action, state),
});

export default rootReducer;
```


## facebook のカンファレンスのアプリのタブ遷移
facebook の[f8](https://github.com/fbsamples/f8app) カンファレンスで利用されていたアプリです。
![recortde3.gif](https://qiita-image-store.s3.amazonaws.com/0/26095/10f2c962-764c-413f-03a1-42583cbd52c2.gif)


全体の画面遷移は F8Navigator.js と言うファイルで、管理されていました。名前からして [非推奨のパッケージ（？）](https://www.npmjs.com/package/react-native-deprecated-custom-components)を使っているようです。routing の管理も if 文で出し分けしています。

```js:F8Navigator.js
...
import { Navigator } from "react-native-deprecated-custom-components";

...

  renderScene: function(route, navigator) {
    if (route.allSessions) {
      return <SessionsCarousel {...route} navigator={navigator} />;
    } else if (route.session) {
      return <SessionsCarousel session={route.session} navigator={navigator} />;
    } else if (route.filter) {
      return <FilterScreen navigator={navigator} {...route} />;
    } else if (route.friend) {
...
```

下にあるタブの遷移には [react-native-tab-navigator](https://github.com/happypancake/react-native-tab-navigator) を使っています。 この記事の上の方で紹介した  [react-native-scrollable-tab-view](https://github.com/skv-headless/react-native-scrollable-tab-view) と似ていますね。(スター数的には [react-native-scrollable-tab-view](https://github.com/skv-headless/react-native-scrollable-tab-view) の方が４倍くらい多い)

[f8app/blob/master/js/tabs/F8TabsView.js](https://github.com/fbsamples/f8app/blob/master/js/tabs/F8TabsView.js)

```js:F8TabsView.js
...
import TabNavigator from "react-native-tab-navigator";
...

    return (
      <TabNavigator tabBarStyle={styles.tabBar}>
        <TabNavigator.Item title="Schedule">
         {/* タブコンテンツ１ */}
        </TabNavigator.Item>
        <TabNavigator.Item title="My F8">
          {/* タブコンテンツ2 */}
        </TabNavigator.Item>
     </TabNavigator>
    )
...
```

GIF 画像中の Demo と言う名前のページ画面のスワイプには android と ios で別の実装がしてありました。
android 版では [ViewPagerAndroid](https://facebook.github.io/react-native/docs/viewpagerandroid.html) を利用して、左右のフリック動作で移動できるようにしているようです。一方 iOS では [ScrollView の horizontal](https://facebook.github.io/react-native/docs/scrollview.html#horizontal) プロパティを利用して、左右の遷移を実現していました。
[f8app/blob/master/js/common/ViewPager.js](https://github.com/fbsamples/f8app/blob/master/js/common/ViewPager.js) ざっくり抜粋すると以下のような実装です。

```js:android
...
import { ViewPagerAndroid } from "react-native";
...

  renderAndroid() {
    return (
      <ViewPagerAndroid
        onPageSelected={this.handleHorizontalScroll}
      >
        {this.renderContent()}
      </ViewPagerAndroid>
    );
  }
```

```js:ios
...
import { ScrollView } from "react-native";
...

  renderIOS() {
    return (
      <ScrollView
        horizontal={true}
        pagingEnabled={true}
        onScroll={this.handleHorizontalScroll}
      >
        {this.renderContent()}
      </ScrollView>
    );
  }
```
（facebook のアプリということもあってスター数が多いリポジトリですが、機能が多すぎるためか、中身は結構見にくい印象でした。。。）

## その他 画面遷移系のライブラリ
### [react-native-router-flux](https://github.com/aksonov/react-native-router-flux)
よく聞くライブラリなので、参考のアプリがサクッと見つかるかなと思ったんですが、[React-Native-Apps](https://github.com/ReactNativeNews/React-Native-Apps) に最近追加されたアプリは、ほとんど react-navigation を利用していたので、今回は深く紹介しません。

サンプルを見る限り、 react-router と似た構成で書けるようなので、WEB 開発をしている人にはとっつきやすそうなライブラリに思えます。管理方法も、ルーティングとコンポーネントを書くスタイルでわかりやすいです。

```js:sample
const App = () => (
  <Router>
    <Stack key="root">
      <Scene key="login" component={Login} title="Login"/>
      <Scene key="register" component={Register} title="Register"/>
      <Scene key="home" component={Home}/>
    </Stack>
  </Router>
);
```


### [react-native-navigation](https://github.com/wix/react-native-navigation)
wix が作っているライブラリで動きが活発。github の README を読むと v2 への書き換えを行なっている模様。

[React Native Advent Calendar 2017](https://qiita.com/advent-calendar/2017/react_native)５日目の記事で、 @hotchemi さんが[QuipperにおけるReact Native活用事例](http://quipper.hatenablog.com/entry/2017/12/05/114014) で書かれていますが、遷移のアニメーションなどが豊富なようなので、その辺りをこだわりたいならこちらを使うのが良さそうです。

## まとめ
画面遷移一つとっても、バリエーション豊富な実装がありました。どれを使うかはアプリの規模や目的によって最適なものを選ぶ必要がありそうです。最近のアプリでは react-navigation での実装が多いようでしたが、react-router-flux は v4 のバージョンアップで MobXフレンドリーにします。的なことが [README](https://github.com/aksonov/react-native-router-flux#v4-features) に書かれていたりするので、状態管理のライブラリと合わせて、選定するのが良い気がします。

個人的には [HackerBuzz-ReactNative](https://github.com/RCiesielczuk/HackerBuzz-ReactNative) の構成がパッと見スッキリしているのと react redux の構成で作っているので好きです。



## 余談: WEB の SPA 開発と React Native の画面遷移を比べて
ネイティブアプリはいきなり特定のページにランディングして来ることが、WEBよりはるかに少ないためか、どこからでも入れることを考慮しなくて良いのは楽な気がしました。しかしその反面、画面遷移のロジックが WEB より複合的（スタックの遷移、タブの遷移）かつバリエーション豊富なため、少々難解な印象を受けました。（ちなみに、WEBの SPA 開発でもネイティブの開発と同じように`戻るボタン`を上部の AppBar に取り付けて history back のアクションで戻ったりすると、悲惨な結果になります。WEBはメールからのリンクや、 検索流入など URL を通して他の世界からくるので、戻る ≒ 他の世界に戻る。になる。特に自分は Cordova でアプリを作っているので、その辺でめっちゃハマりました。）