React Navigation 3.x チートシートつくってみた

# React Navigationとは
[React Navigation](https://reactnavigation.org/)とはReact Navtiveのページ遷移をスムーズに実装できるライブラリの1つです。
遷移だけでなく、それに付随した機能も数多く存在するのも特徴の1つ。

同系統のライブラリとしてreact-native-router-fluxなどとよく比較されますが、React Navigationの方がドキュメントやサンプルが多いのでおすすめ。

ドキュメントが豊富なのはいいですが、書き方が統一されておらず、1つ1つの機能がわかりずらかったり非推奨のAPIが存在するなど、分かりづらいポイントがあるので、
今回は広く浅くできることをチートシートとしてまとめてました。

触れてない機能などは随時更新する予定です。

# React Navigationの基本
nativeはwebのようなURL遷移ができないため、1ページ1ページをルーティングしてあげる必要があります。
そのルーティング設定の際に遷移の種類や共通コンポーネント(ヘッダーやタブなど)を設定できるのでかなり直感的に作業できます。

**この記事で触れる機能は**

- **`'react-navigation'`からimportできる　ルーティングAPI群、**
- **それに付属するProp群、**
- **それ以外の機能**

**の3つに分けて紹介します。**

# ルーティングAPI
ルーティングAPIで正しくページの構成などの初期設定をすると、後から自由自在にページ遷移できるようになります。

ルーティングAPIでルート処理をするとpropとして他APIや各種フラグなどの受け取りが可能になります。
また、React Navigationが提供しているコンポーネントを使うことで開発スピードをあげることもできます。

ルーティングAPIは全部で7種類(2019/5現在)用意しています。

使い方はどれも基本的には同じですが、オプションの設定だけ大きく変わりますのでご注意ください。

```routes.js
//createStackNavigator APIを使っているが他ルーティングAPIでも使い方は同様

import { createStackNavigator } from 'react-navigation'

import MainScreen from './MainScreen.js'
import DetailScreen from 'DetailScreen.js'

export default = createStackNavigator({
	Stack1: {
		screen: MainScreen,
	},
	Stack2: {
		screen: DetailScreen,
		//ここにscreen毎のオプションを追加できるよ
	}
},
{
	//ここではルーティング毎のオプションが追加できるよ
})
```

ルーティングAPIで作成されたオブジェクトはそのままコンポーネントとして使えます。

```App.js
import App from '.../routes.js';

export default () => <App />
```

以下は全ルーティングAPIとその**特徴**
、使い道です。
*細かいオプションがたくさんあるのでそちらは公式ドキュメントを参考にしてください。


## ルーティングAPI(7種)
### 1.createStackNavigator
**使用用途**

- 遷移元に戻ることを前提としたページ間。<br>
**(遷移後も下のページは描写されたままになります。したがって、戻ったあとに再レンダリングは起きません)**

**特徴**

- ページの上にページを重ねるイメージ。
- デフォルトでスワイプによるバックとアニメーション付き。
- デフォルトでヘッダー設置。

### 2.createDrawerNavigator
**使用用途**

- ドラワーメニューのみ。(正確には遷移ではない)

**特徴**

- 遷移ではないが、createStackNavigatorの下が少し見えるイメージ。
- デフォルトでスワイプによるバックとアニメーション付き。
- デフォルトでヘッダーとハンバーガーメニュー設置。

### 3.createSwitchNavigator
**使用用途**

- 遷移後戻ることを想定してない、ページ間。<br>
**(例えば、ログインページや起動時のローディングページなど一度訪れたら、再び戻ることを想定していないページ間)**

**特徴**

- createSwitchNavigatorで作られたページは常に1つしか描写されず、切り替え時に片方を削除してもう片方をレンダリングする。
 
### 4.createAnimatedSwitchNavigator
**使用用途**

- 遷移後戻ることを想定してない、ページ間。<br>
**(例えば、ログインページや起動時のローディングページなど一度訪れたら、再び戻ることを想定していないページ間)**

**特徴**

- 3のcreateSwitchNavigatorにアニメーションが付与されました。
- react-native-reanimatedというライブラリの1.0.0以上をインストール必要がある。
- アニメーションの種類、時間、イージング設定ができる。
 

###  ~~4.createTabNavigator~~
>**使用用途**
>
>- 並列に配置されたページ間で使われる。<br>
>(Twitterでいう「TL、検索、通知、DM」が並列ページ)
>
>**特徴**
>
>- **非推奨**<br>
>**(後述の`createBottomTabNavigator`か`createMaterialTopTabNavigator `で代用可能)**
>- 設定でスワイプによる遷移とアニメーション可能。
>- 並列ページは初期表示時に全て表示される。→初期表示の遅延が発生しやすい

**こちらは完全に削除されました。**(2019/4/1)


### 5.createBottomTabNavigator
**使用用途**

- 並列に配置されたページ間で使われる。

**特徴**

- アニメーションやスワイプ不可。
- 並列ページは１ページのみレンダリングされ、他ページは遷移時に初めてレンダリングされる。→初期表示速度が早い

### 6.createMaterialBottomTabNavigator
**使用用途**

- 並列に配置されたページ間で使われる。

**特徴**

- 仕様はcreateTabNavigatorとほぼ同じ。
- 追加ライブラリ(`react-navigation-material-bottom-tabs`)をいれることでタブをマテリアルデザインにできる。

### 7.createMaterialTopTabNavigator
**使用用途**

- 並列に配置されたページ間で使われる。

**特徴**

- デフォルトでスワイプによる遷移と遷移アニメーションやタブ下ラインのアニメーション完備。
- 並列ページは初期表示時に全て表示される。→初期表示の遅延が発生しやすい。
- 名前にTopTabとあるが、tabBarPosition: 'bottom'でボトムタブとして使うことが多い。(個人的意見)
- **タブアニメーションやスワイプを入れたいのであれば現状これ一択。**

## ルーティングAPIのネスト

```js
const nestedScreen = createStackNavigator({
    nestedStack: {
        screen: MainScreen,
    }
}
export default = createStackNavigator({
    Stack1: {
        screen: nestedScreen
    },
    Stack2: {
        screen: DetailScreen
    }
}
```

例えば、スタック遷移をネストさせたいのあれば上記でOK。


使用時に注意していただきたいことは、大規模開発でルーティングAPIを設計なしに使っていくと、
飛びたいのに飛べないページやスクリーン名の重複による予期せぬ挙動が生まれたりするので、
設計は注意深くするべきです。

また、デフォルトのヘッダやフッタが付いてくるので、非表示にするなり、自作のものを使わないと、
気づかぬうちに画面を覆います。

# Props群
ルーティングAPIで作成されたコンポーネントには`props.navigation`オブジェクトが渡されます。
そのオブジェクトの中に後述のメソッドやステート、パラメータなどが格納されてますので、
props.navigationには非常にお世話になることでしょう。
大前提として、遷移できるページは兄弟ページとしてルーティングされたページ間だけです。
兄弟ページとは並列ページ(タブなど)のことではなく、何かしらのルーティングAPIで結ばれたページ間のことです。

ネストされたページに関しては深層部から表層部のページ遷移を実行させることもできます。
## navigation.navigate()
- 対象ページに遷移

```js
navigation.navigate({
  routeName: 'NextPage',
  params: {id: 12345},
  action: NavigationActions.navigate({ routeName: 'DetailScreen' })
  key: '1'
})
//routeName:遷移先のスクリーン名。String型
//params:遷移先のページから受け取れるパラメータ。Object型　*後述
//action: NavigationActions API *後述
//key:ページバックの時に使うユニークキー　*後述

//keyを使わないのであれば以下のように第1引数~第3引数として省略できる。
navigation.navigate('NextPage', {id: 12345}, NavigationActions.navigate({ routeName: 'DetailScreen' }))
```
actionを使うことによって、並列ページの子ページの子ページというように、自由にページ移動できる。
また、NavigationActions.navigate()の引数を設定すれば問題なくparamsも届けられる。

keyを設定しておくことで、 NavigationActions.backでいつでも戻ってこれます。

## navigation.dispatch()
上記で出てきたNavigationActionsというアクションクリエータと合わせて使います。
ただの遷移であれば、dispatchを使うよりもnavigation.navigateを使った方が圧倒的に楽です。

```js
import { NavigationActions } from 'react-navigation';

const navigateAction = NavigationActions.navigate({
  routeName: 'Profile',
  params: {},

  // ここもネストできるよ
  action: NavigationActions.navigate({ routeName: 'SubProfileRoute' }),
});
this.props.navigation.dispatch(navigateAction);
```

ユニークキーを使ってページバックしたい場合は下記のように書くと便利です。
webでいうクエリパラメータを使ったようなバックができます。

```js
import { NavigationActions } from 'react-navigation';

const backAction = NavigationActions.back({
  key: 'Profile',
});
this.props.navigation.dispatch(backAction);
```

## navigation.goBack()
- 特定ページを指定せずともページバックができます。
ページバックですので、上位の階層にしかもどれません。
つまり、タブ切り替えなどの兄弟ページから遷移してきたとしても、goBack()を使えば、親ページに飛びます。

```js
navigation.goBack() //1ページ前に戻る
navigation.goBack(null) //戻る先がない場合は親ページのルーティングから戻る
```

## navigation.addListener()
addListener()を使うと、ナビゲーションのライフサイクルを後付けで追加できる。
条件分岐が激しくないのであれば、後述のonNavigationStateChangeオプションを使った方がスッキリしなくもない。

ライフサイクルは以下の4つ。
`willFocus `:the screen will focus
`didFocus `:the screen focused (if there was a transition, the transition completed)
`willBlur `:the screen will be unfocused
`didBlur `:the screen unfocused (if there was a transition, the transition completed)

試しに、page1からpage2に遷移させてみたところ以下の結果になった。
<img width="279" alt="スクリーンショット 2019-06-09 20.53.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234092/aa3b19b1-6068-6b59-3539-9b06ce193b2a.png">
どうやら、BlurよりFocusの方が早く呼ばれるようだ。
もしかしたら、`didFocus`と`willBlur`が逆転すると期待していたが、そうはならないらしい。

また、willとdidの明確な違いが知りたいが、わからない。 
わかる方がいましたらコメントください。。。

```js
//一度登録すると、以降そのスクリーンに遷移した(正確にはisFocusedがtrueになった)際処理が走る。
//注意したいのはレンダーの際には走らないこと。
this.props.navigation.addListener('didFocus', () => console.log('フォーカス時'))

// componentDidMountと一緒に使うことで、初期レンダリングでも呼ばれる。
componentDidMount() {
  this.props.navigation.addListener('didFocus', () => console.log('フォーカス時'))
  console.log("レンダリング時")
}

//第一引数で、諸々のステートを取れる
this.props.navigation.addListener('didFocus', payload => console.log(payload))
```

関連：
[NavigationEventsコンポーネント](https://qiita.com/ibaragi/private/13ef0dad6326cc6a7fb5#navigationevents%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88)
[onNavigationStateChangeオプション](https://qiita.com/ibaragi/private/13ef0dad6326cc6a7fb5#onnavigationstatechange%E9%81%B7%E7%A7%BB%E3%81%94%E3%81%A8%E3%81%AB%E5%87%A6%E7%90%86%E3%82%92%E5%99%9B%E3%81%BE%E3%81%9B%E3%82%8B)

## navigation.isFocused()
特定ページをフォーカスした時にtrueを返す。それ以外はfalse。
スタックのように、裏で描写はされているが、フォーカスはされていない
という場面があるので、その状態を知れる。

```js
this.props.navigation.isFocused() //true
```
# ルーティングAPIとProps以外の機能
## onNavigationStateChange属性
ルーティングAPIで設定しているコンポーネントなら、onNavigationStateChangeオプションを使うことで、その配下で遷移が起きる度にイベントを発火させることができる。
ページのインデックスも引き数として取れるので、アナリティクスにも使える。
reduxActionも返してくれるので、reduxを更新したい場合はアクションを自前で作る必要はなさそうです。
引数サンプル：`onNavigationStateChange(prevState, currentState, reduxAction)`


```js
<App onNavigationStateChange={(prevState, currentState, reduxAction) => {
  console.log(prevState, currentState, reduxAction)}>
}
```

上のonNavigationStateChangeを使ってスクリーンネームを表示させる処理

```js

const getActiveRouteName = (navigationState) => {
  if (!navigationState) {
    return null
  }
  const route = navigationState.routes[navigationState.index]
  // 展開したnavigationStateの中にrouteオブジェクトがあれば、さらにネストされたスクリーン名を返す。
  if (route.routes) {
    return getActiveRouteName(route)
  }
  return route.routeName
}

export default () => (
  <App
    onNavigationStateChange={(prevState, currentState) => {
      console.log(getActiveRouteName(prevState))
      console.log(getActiveRouteName(currentState))
    }}
  />
)
```
スクリーンネームを取るだけなら、reduxActionのrouteNameに入っているので、
`reduxAction.routeName`をとった方が早そう。

## NavigationEventsコンポーネント
[navigation.addListener()](https://qiita.com/ibaragi/private/13ef0dad6326cc6a7fb5#navigationaddlistener)で使用したライフサイクルをNavigationEventsコンポーネントにpropとして渡すことで、処理してくれる。
初回レンダリング時にも、もちろんよばれる。

```js

import { NavigationEvents } from 'react-navigation';
  <View>
    <NavigationEvents
      onWillFocus={payload => console.log('will focus',payload)}
      onDidFocus={payload => console.log('did focus',payload)}
      onWillBlur={payload => console.log('will blur',payload)}
      onDidBlur={payload => console.log('did blur',payload)}
    />
    {/* 他コンポーネント */}
  </View>
```

## FlatListコンポーネント
使い方はReactNativeのFlatListと同じ。
ただ、ReactNavigationのタブと併用すると、タブを押すことで、listのtopに自動でスクロールされる。

---

# アップデート情報
2019/4/1: createTabNavigatorが使えなくなっていたので、追記
2019/5/1: createSwitchNavigator追記(まだ使ってない)

# 参考：
[React Navigation 3.x Doc]
(https://reactnavigation.org/docs/en/getting-started.html)
[React Navigation 3.x API](https://reactnavigation.org/docs/en/scrollables.html)

# あとがき
Twitter気軽にフォローください〜
[イバラギのTwitter](https://twitter.com/iba_ragi)