react-native-router-flux v4 の実装7パターンをコードとGIFで眺める

この記事は [React Native Advent Calendar 2017](
https://qiita.com/advent-calendar/2017/react_native) 22日目です。


ReactNativeの開発において不可欠なナビゲーションライブラリ。
その主要なものの一つである [react-native-router-flux](https://github.com/aksonov/react-native-router-flux) (RNRF) は現在betaとしてv4が提供されています。


今回はそのv4が提供する、画面・ナビゲーションに関わるコンポーネント7つを、 https://github.com/aksonov/react-native-router-flux/blob/master/docs/API.md を見ながら足りない情報をコードから補いつつ順に見ていきます。

対象のコンポーネントはこちら。

- Scene
- Stack
- Tabs
- Modal
- Drawer
- Lightbox
- Overlay

## (おさらい)　RNRF の 画面遷移
v4になっても、画面遷移の基本は変わりません。

```javascript
// 1. homeというkeyを持つ画面（Scene）に移動する
Actions.home()

// 2. 一つ前の画面に戻る
Actions.pop()

// 3. 今の画面を更新する
Actions.refresh()
```

ボタンなどをおした際のonPressなどに上記を仕込めば画面遷移させることができます。

なおこの先のサンプルコードにおいてcomponentに渡されるものは概ね以下のようなコンポーネントです。


```javascript

import React, { Component } from 'react';
import { View, Text, StyleSheet } from "react-native";
import Button from "react-native-button";
import { Actions } from "react-native-router-flux";

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#F5FCFF",
  },
});

export default class extends Component {
  render() {
    return (
        <View style={styles.container}>
            {/* タイトルはコンポーネント毎に編集し、またボタンの数やonPressの内容は適宜変更します */}
            <Text>Home</Text>
            <Button onPress={() => { Actions.profile() }}>next</Button>
        </View>
    );
  }
}
```

<img width="377" alt="base.png" src="https://qiita-image-store.s3.amazonaws.com/0/43333/3cfde38a-fb7d-d13a-306f-93238fc02ccd.png">

こんなかんじ。
では下準備が終わったところで早速一つ一つ見ていきます。


## Scene
> The basic routing component for this router, all <Scene> components require a key prop that must be unique. A parent <Scene> cannot not have a component as a prop as it will act as a grouping component for its children.

ルーティングの基本となるcomponentです。
アプリ全体でユニークなkeyを必須で持ち、 たとえば`home`というkeyを持つSceneには`Actions.home()`で遷移できます。

幾つかのSceneをグルーピングする親と、自身がcomponentを持つ子の2種類にわけられ、前者は自身が直接componentを持つことはありません。


```javascript
<Router>
    <Scene 
        key='main' // 親はcomponentを持たない
    >
        <Scene 
            key='login'
            component={Login}
        />
        <Scene 
            key='home'
            initial={ true } // 隣接するsceneたちの中で最初に表示される
            component={Home}
        />
        <Scene 
            key='home'
            component={Home}
        />
    </Scene>
</Router>
```

### 動かすと...
<img src='https://study-log.s3.amazonaws.com/uploads/attachment/attachment_path/238/scene.gif'>



## Stack
> A component to group Scenes together for its own stack based navigation. Using this will create a separate navigator for this stack, so expect two navbars to appear unless you add hideNavBar.

子Sceneをスタックさせる親コンポーネントです。
Stackで囲むと新しいスタックを設け、navbarを表示することができます。
navbarが不要であればSceneで囲ってあげるとよいです。


```javascript
<Router>
    <Scene
        key='main'
    >
        <Scene
            key='login'
            component={Login}
        />
        <Stack
            key='inner'
        >
            <Scene
                key='home'
                component={Home}
            />
            <Scene
                key='profile'
                component={Profile}
            />
        </Stack>
    </Scene>
</Router>
```

### 動かすと...
<img src='https://study-log.s3.amazonaws.com/uploads/attachment/attachment_path/239/stack.gif'>

Stackの箇所で新たにnavbarが生じ、二重に表示されていることがわかります。
Stackを複数個並列にもち、その親Sceneには `hideNavBar`をもたせる、といった構成はRNRFのサンプルコードにもでてきます。

## Tabs
> Can use all props listed above in <Scene> as <Tabs> is syntatic sugar for <Scene tabs={true}>.

 `<Tabs />` は `<Scene tabs>` のシンタックスシュガーです。
自身は親Sceneであり、componentを直接は持たず、内包した子Sceneをタブで表示します。

RNRFはv4からreact-navigationの機構を採用しており、このコンポーネントはreact-nativagationのTabNavigatorが持つpropsを利用できます。
(なぜそのようになったのか / react-navigationとの違いはどこにあるのか、については https://github.com/aksonov/react-native-router-flux/issues/2001 ここが詳しいです)

react-navigation側の該当ドキュメントはこちら
https://reactnavigation.org/docs/navigators/tab#TabNavigatorConfig 

上記URL先にprops一覧がありますが、冒頭に上げたRNRFのapiドキュメントにも記載があるのでそこだけ見るのでも良いかと思います。

```javascript
<Router>
    <Tabs 
        key='main'
        swipeEnabled={ true } // スワイプでタブを移せる
        animationEnabled={ false } // スワイプ時にアニメーションを付ける。個人的にはあまり好きでないのでfalseにしてオフっている
    >
        <Scene 
            key='login'
            component={Login}
            tabBarLabel='ログイン'
            icon={ () => (<Text>ログイン</Text>)} // 本当はreact-native-vector-iconsなどをつかってアイコンを渡す...
        />
        <Scene 
            key='home'
            initial={ true } // 隣接するsceneで最初に表示される
            component={Home}
            tabBarLabel="ホーム"
        />
        <Scene 
            key='home'
            component={Home}
            tabBarLabel="プロフィール"
        />
    </Tabs>
</Router>
```

### 動かすと...

<img src='https://study-log.s3.amazonaws.com/uploads/attachment/attachment_path/242/tabs.gif'>

タブの色の変化が見づらいですね...ですが、SceneやStackとは異なりnabvarが登場せず、タブが移り変わっていることがわかるかと思います。

また `swipeEnabled={ true }` にしたことでスワイプでタブ移動ができるようになっています。 

ここまではつまづきも少ないですが、ここから少し複雑になります。


## Modal

> To implement a modal, you must use <Modal> as the root scene in your Router. The Modal will render the first scene (should be your true root scene) normally, and all following To display a modal use <Modal> as root renderer, so it will render the first element as normal scene and all others as popups (when they are pushed).


 `<Modal />` は `<Scene modal>` のシンタックスシュガーです。
自身は親Sceneであり、componentを直接は持たず、内包した子Sceneを通常表示・モーダル表示します。

重要なのは、**Modal直下最初のSceneのみ通常通り表示され、それ以外のSceneはpush時にmodalとして表示**される点です。

なので、モーダル表示したいSceneはModalのとじタグ直前に並べておくことになります。

```javascript
<Router>
    <Scene 
        key="root"
        hideNavBar={ true }>

        <Modal
            hideNavBar={ true }>

            {/* Modal直下最初のsceneのみ通常表示 */}
            <Scene
                key='main'
            >
                <Scene
                    key='home'
                    component={Home}
                />
                <Scene
                    key='profile'
                    component={Profile}
                />
            </Scene>

            {/* Modal直下二つ目以降はpush時にmodal表示される */}
            <Scene
                key='settings'
                component={Settings}
            />
            <Scene
                key='error'
                component={Error}
            />
        </Modal>

        {/* Modalの外なので関係なし */}
        <Scene
            key='login'
            component={Login}
            tabBarLabel='ログイン'
            icon={ () => (<Text>ログイン</Text>)} // 本当はreact-native-vector-iconsなどをつかってアイコンを渡す...
        />

    </Scene>
</Router>
```

### 動かすと...

<img src='https://study-log.s3.amazonaws.com/uploads/attachment/attachment_path/243/drawer.gif' >

Modal内2番目以降のSceneのみモーダル表示されていることが見て取れます。
LoginはModalの外なのでモーダルではなく通常の表示になっていますね。

## Drawer
> Can use all prop as listed in Scene as <Drawer>, syntatic sugar for <Scene drawer={true}>


`<Drawer />` は `<Scene drawer>` のシンタックスシュガーです。
自身は親Sceneであり、同時にドロワー部分のcomponentを持ちます。
また、内包した子Sceneを通常表示します。

```javascript
<Router>
    <Scene 
        key="root"
        hideNavBar={ true }>

        <Drawer
            key="drawer"
            // drawerImage={() => (<Image/>)} // デフォルトのハンバーガーメニューを差し替える
            // drawerIcon={() => (<Icon/>)} // デフォルトのハンバーガーメニューを差し替える
            drawerWidth={ 300 }
            contentComponent={DrawerContent} // ドロワー部分のcomponentを渡している
        >
          <Scene
              key='login'
              component={Login}
          />
          <Scene
              key='home'
              component={Home}
          />
          <Scene
              key='profile'
              component={Profile}
          />
        </Drawer>

        {/* Drawerの外なのでドロワーボタンは表示されない */}
        <Scene
            key='settings'
            component={Settings}
        />

    </Scene>
</Router>
```

### 動かすと...

<img src='https://study-log.s3.amazonaws.com/uploads/attachment/attachment_path/244/drawer.gif'>

ドロワーから遷移した先で `Action.pop()` するとドロワーに戻るのが面白いですね。

## Lightbox
> Lightbox is a component used to render a component on top of the current Scene. Unlike modal, it will allow for resizing and transparency of the background.

`<Lightbox />` は直前までに表示していた画面の上に覆いかぶさるように表示するコンポーネントです。
自身を透明にしたり、リサイズすることができます。

確認画面や簡単なエラーの表示、画像の簡易表示などに使うことが多いコンポーネントです。

Modalと似て、 **Lightbox直下最初のSceneのみ通常通り表示され、それ以外のSceneはpush時にlightboxとして表示**されます。

なので、lightboxとして表示したいSceneはLightboxのとじタグ直前に並べておくことになります。


さて、ここでLightbox用に簡単に新しいサンプルのコンポーネントを導入します。
背景が少し透明な黒色で、その中央にコンテンツを表示する、というスタイルを当てているだけで他に特殊なことはしていません。

```javascript
// LightBoxBase.js

import React from 'react';
import { View, Text, StyleSheet, Button } from 'react-native';
import { Actions } from 'react-native-router-flux';

const styles = StyleSheet.create({
  shadow: {
    backgroundColor: 'rgba(52,52,52,0.5)', // 透明な背景
    position: 'absolute',
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
    justifyContent: 'center',
    alignItems: 'center',
  },
  container: {
    width: 300,
    height: 300,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'white',
  },
});

const LightBoxBase = ({children}) => (
  <View style={styles.shadow}>
    <View style={styles.container} >
      {children}
      <Button title="Close" 
              onPress={() => { Actions.pop() }} // popさせてSceneを閉じる
      />
    </View>
  </View >
);

export default LightBoxBase;
```

これを利用してLightboxを試してみます。


```javascript
const Error = () => (
    <LightBoxBase>
      <Text>error!</Text>
    </LightBoxBase>
)

const Confirm = () => (
    <LightBoxBase>
      <Text>OK?</Text>
    </LightBoxBase>
)

const Example = () => (
    <Router>
      <Lightbox
          key="root"
      >
        {/* Modalと同様、Lightbox直下の最初のSceneのみ通常表示される */}
        <Scene
          key="inner">
          <Scene
              key='login'
              component={Login}
          />
          <Scene
              key='home'
              component={Home}
          />
        </Scene>

        {/* Modalと同様、Lightbox直下の2番目以降のSceneはpush時にLightboxの演出が加えられる */}
        <Scene
            key='error'
            component={Error}
        />
        <Scene
            key='confirm'
            component={Confirm}
        />

      </Lightbox>
    </Router>
);
```

### 動かすと...

<img src='https://study-log.s3.amazonaws.com/uploads/attachment/attachment_path/245/lightbox.gif' >


背景となるSceneが画面中央寄せでわかりにくいですが、黒色半透明なSceneが覆いかぶさるように表示されていることが確認できました。


## Overlay
API.mdに記述はないですが、 https://github.com/aksonov/react-native-router-flux/issues/2285 をみるに、
`<Overlay />` は `<Scene overlay>` のシンタックスシュガーのようです。
しかしoverlayの説明もドキュメントにはありません。
ExampleではRouterの直下に配置されており、気になるコンポーネントです。試しつつコードを眺めてみます。



```javascript
<Router>
    <Overlay
        key="main">
        <Scene
            key='home'
            component={Home}
        />
        <Scene
            key='profile'
            component={Profile}
        />

    </Overlay>
</Router>
```

このコードだとこんな感じ。

<img width="373" alt="overlay1.png" src="https://qiita-image-store.s3.amazonaws.com/0/43333/d9ab69dd-afc0-a3e1-1fe9-e635d97d5054.png">

Overlay内にあるSceneが並んで同時に表示されています。

またコードを簡単に追ってみると

- overlay属性はOverlayNavitationを結局は作っており、
    - https://github.com/aksonov/react-native-router-flux/blob/master/src/navigationStore.js
- OverlayNavigationではinitialのsceneを先頭にこさせるように並び替えた後、各sceneを並べて出力する
    - https://github.com/aksonov/react-native-router-flux/blob/3c81707d5568ec03ae1da001d452ffc069bdeeb0/src/OverlayNavigator.js

としているようにみえます。（もうちょっと丁寧に追う。）

また、イシューを見ると他ライブラリのDrawerと組みあせたいという場面でよく利用されており、サンプルコードではスナックバーを画面に重ね合わせて表示する際に使われています。

ということで、サンプルコードやIssue、コードををみるに、 ２つ以上のSceneを同時に表示させたいとき にOverlayで囲っているようです。

RNRFのDrawer, Lightbox以外で、２つのSceneを同時に表示したいというケースにはOverlayを検討する...ととりあえず覚えて今後使ってみます。


## RNRF(というかナビゲーション)のドキュメント、ちょっとわかりにくい => 小さく試す
様々なroutingライブラリを放浪した結果、個人的に最もシンプルに画面遷移を定義できると感じるRNRFに戻ってきました。

しかし、いかんせんRNRFはAPIのドキュメントが(以前よりはマシとは言え)揃いきっているとはいえず、また動きもコードやドキュメントだけでは把握しきれないところがあります。

幸いRNRFは手厚いサンプルコードを提供しており（そこも好き）、それを丁寧に追うことで使い方はみえてきますが、一方で手厚すぎて、所見ではｳｯ...と感じることもあるかと思います。

※　RNRFが提供するサンプルコードの中心となる箇所 ... 長い
https://github.com/aksonov/react-native-router-flux/blob/master/Example/Example.js 


ということで今回はナビゲーションを提供するコンポーネントを一つ一つばらしてgifつきで見てみました。

皆様のReactNativeライフのお役に少しでも立てていれば幸いです。