React Navigation v5 傾向と対策

## はじめに

この記事は [React Native Advent Calendar 2019](https://qiita.com/advent-calendar/2019/react-native) 10 日目の記事です。
昨日は @tkow さんの記事で、 React Native 0.60 で導入されたネイティブモジュールの autolinking に関する解説でした。

この記事は、 React Navigation の次バージョンである v5 について、機能や変更点などを予習しておこうというような内容です。

主な情報源は以下になります。


- [GitHub](https://github.com/react-navigation/navigation-ex) の README とソースコード
- [公式ドキュメント(next)](https://reactnavigation.org/docs/en/next/getting-started.html)
- [Expoのブログ](https://blog.expo.io/announcing-react-navigation-5-0-bd9e5d45569e) 

GitHub や公式ドキュメントは正式リリース後にリンク切れになる可能性があります。

## v4 までのあらすじ

[React Navigation](https://reactnavigation.org/en/) は [React Native 公式ドキュメントのナビゲーションのページ](https://facebook.github.io/react-native/docs/navigation) にも一番目に紹介されている、 React Native でナビゲーションを行う際の最もスタンダードなライブラリです。 
ネイティブモジュールへの依存が比較的少なく、 Expo でも簡単に動かすことができるのが特徴です。

ここで、 React Navigation の今までの大まかな変遷を振り返ってみます。

#### beta (2017/02)

- 詳しい状況は知らないのですが、正式リリースまでの期間が長かったため beta だったにも関わらず本番投入されることが多い印象でした

#### v1 (2018/02)

- 今まで推奨されていた Redux 統合が deprecated になり、 [react-navigation-redux-helpers](https://github.com/react-navigation/redux-helpers) として分離
- 人気のあった [react-native-router-flux](https://github.com/aksonov/react-native-router-flux) が v4 から `react-navigation` に依存するようになり、ナビゲーションライブラリの事実上のスタンダードとしての地位を確立

#### v2 (2018/05)

- `XNavigator(...)` が deprecated になり、代わりに `createXNavigator(...)` を使うようになる
- `TabBar` が `createBottomTabBar`, `createMaterialTopTabBar`, `createMaterialBottomTabBar` に分割

#### v3 (2018/11)

- トップレベルのナビゲーションを `createAppContainer` で囲うように変更
- 内部的にナビゲーションの実装を `react-navigation-stack`, `react-navigation-tabs`, `react-navigation-drawer` パッケージに分離
- 途中で `react-navigation` パッケージに TypeScript の型情報追加

#### v4 (2019/09)

- v3 で分離されたパッケージが `react-navigation` を通して提供されなくなり、個別にインストールするように変更


メジャーバージョンが変わるごとに互換性のない変更があったものの、基本的にバージョンアップに必要な作業は関数名やパッケージ名の変更程度でした。

それでは、 v5 の変更点を見ていきましょう。

## ナビゲーションを JSX で定義するようになり、動的に変化させることが可能になる

#### v4 以前

v4 までは、以下のように `createXNavigator` 関数ににオブジェクトを渡してナビゲーションを定義していました。 v1 では `XNavigator` という名前の関数でしたが、基本は同じでした。

```tsx
const StackNav = createStackNavigator({
  Home: { screen: HomeScreen },
  Profile: { screen: ProfileScreen },
  Settings: { screen: SettingsScreen }
});
```

この `createXNavigator` 関数は render 内で呼んではならず、予め定義しておく必要がありました。そのため例えばニュースアプリ等で動的にカテゴリ一覧を取得してカテゴリ毎にタブを表示したい等といった場合は、ナビゲーションを表示する前に予めカテゴリ一覧を取ってきて、クラスコンポーネントのコンストラクタでナビゲーションを定義する等工夫する必要がありました。

#### v5

v5 では、以下のように render 内に JSX でナビゲーションを定義するようになります。 Web の React でよく使われる `react-router` や前述の `react-native-router-flux` に近い書き味です。

```tsx
const Stack = createStackNavigator();

const StackNav = () => (
  <Stack.Navigator>
    <Stack.Screen name="Home" component={HomeScreen} />
    <Stack.Screen name="Profile" component={ProfileScreen} />
    <Stack.Screen name="Settings" component={SettingsScreen} />
  </Stack.Navigator>
);
```

`createXNavigator` が `Navigator` と `Screen` というコンポーネントを返すので、それを使って定義します。 `React.createContext` が `Provider` と `Consumer` を返す関係に似ています。記述量は v4 までに比べて若干増えてしまっています。

なんとこの書き方では通常のコンポーネントと同様に条件分岐やループが可能です。

```tsx
const TabNav = () => (
  const tabs = useState(['Tab1', 'Tab2']);

  return (
    <Tab.Navigator tabBarOptions={{ scrollEnabled: true }}>
      {tabs.map(tab => (
        <Tab.Screen key={tab} name={tab}>
          {() => (
            <View><Text>{tab}</Text></View>
          )}
        </Tab.Screen>
      ))}
    </Tab.Navigator>
  );
);
```

この例では、 `tabs` を state として定義していますので、要素が増減するとナビゲーションもそれに追従します。

また、 この書き方のように Screen の中身を `component` ではなく子コンポーネントに Render Callback として渡すことも可能です。こうすることで動的に props を渡すことが可能になりますが、 `component` に渡した場合は不要な render が呼ばれないようにライブラリ側で最適化してくれるので、基本的には `component` に渡すのが推奨なようです。 （[READMEより](https://github.com/react-navigation/navigation-ex#basic-usage)）

## オプションを render 内でセットできるようになる

#### v4 以前

v4 までは、 `navigationOptions` も render 外で予め定義しておく必要がありました。コンポーネント側に設定する方法とナビゲーション側に設定する方法がありましたが、どちらにしてもコンポーネント内とデータをやりとりするには以下のように [params を介してハンドラを渡す](https://reactnavigation.org/docs/en/header-buttons.html#header-interaction-with-its-screen-component) 必要がありました。

```tsx
const EditScreen = props => {
  const { navigation } = props;

  const onSubmit = useCallback(...);

  useEffect(() => {
    // setParams する条件に気をつけないと無限に render されるので注意!!
    if (onSubmit !== navigation.getParam('onSubmit')) {
      navigation.setParams({ onSubmit });
    }
  }, [navigation, onSubmit]);

  // ...
};

// コンポーネント側で設定する場合
EditScreen.navigationOptions = ({ navigation }) => ({
  title: 'Edit',
  headerRight: () => (
    <Button onPress={navigation.getParam('onSubmit')}>
      <Text>Submit</Text>
    </Button>
  )
});


const StackNav = createStackNavigator({
  Edit: {
    screen: EditScreen,
    // ナビゲーション側で設定する場合
    navigationOptions: ({ navigation }) => ({
      title: 'Edit',
      headerRight: () => (
        <Button onPress={navigation.getParam('onSubmit')}>
          <Text>Submit</Text>
        </Button>
      ),
    }),
  },
  // ...
});
```

#### v5

v5 では render 内で `navigation.setOptions` を呼び出してオプションをセットする事が可能になります。

```tsx
const EditScreen = props => {
  const { navigation } = props;

  const onSubmit = useCallback(...);

  // コンポーネント内で設定する場合
  navigation.setOptions({
    title: 'Edit',
    headerRight: () => (
      <Button onPress={onSubmit}>
        <Text>Submit</Text>
      </Button>
    )
  });

  // ...
};

const Stack = createStackNavigator();

const StackNav = () => (
  <Stack.Navigator>
    {/* ナビゲーション側で設定する場合 */}
    <Stack.Screen name="Edit" component={Edit} options={{ ... }} />
  </Stack.Navigator>
);
```

がっつりと副作用を起こしているように見えるのですがドキュメントを見る限り `useEffect` の中ではなく render 直下に書くようです。パフォーマンス面までは検証が間に合いませんでした。

また、引き続きナビゲーション側でも `options` として渡すことができますが、この場合は従来どおり `getParam` を介する必要があるので、動的なオプションの場合コンポーネント内で `setOptions` したほうが良さそうです。

## TypeScript 対応強化

#### v4 以前

型の当て方は色々な方法がありますが、 v4 で関数コンポーネントとして定義する場合は以下のように書くとシンプルです。

```tsx
type Params = {
  id: string
};

type ScreenProps = {};

const DetailScreen: NavigationStackScreenComponent<
  Params,
  ScreenProps,
> = props => {
  const { navigation } = props;

  // 型推論が効く
  const id = navigation.getParam('id');

  const onPress = useCallback(() => {
    // routeName は string, params は any
    navigation.navigate('Comments', { id });
  }, [navigation]);
  
  // ...
};
```

そのコンポーネントがマウントされるスクリーンの params や screenProps を定義することはできましたが、他のスクリーンへの navigate のときに routeName や params に正しい型情報を与えるのは困難でした。

#### v5

ナビゲーション全体を表す ParamList という型を先に定義しておき、それを使う形に変更になります。

```tsx
type StackParamList = {
  Home: undefined;
  Detail: { id: string };
  Comments: { id: string };
};

type DetailScreenNavigationProp = StackNavigationProp<StackParamList, 'Detail'>;
type DetailScreenRouteProp = RouteProp<StackParamList, 'Detail'>;

type Props = {
  navigation: DetailScreenNavigationProp;
  route: DetailScreenRouteProp;
};

const DetailScreen: React.FC<Props> = props => {
  const { navigation, route } = props;

  // 型推論が効く
  const { id } = route.params;

  const onPress = useCallback(() => {
    // routeName の型推論が効き、 params に id が必須になる
    navigation.navigate('Comments', { id });
  }, [navigation]);

  // ...
}

```

`navigation.navigate` に型推論が効くのは嬉しいです。ただし、ネストしたナビゲーションの場合 `CompositeNavigationProp` を使って NavigationProp をマージしていくのですが、そちらで試したところ現在は `navigation.navigate` の型推論が効きませんでした。
書き方としては画面ごとに NavigationProp と RouteProp を定義していくのは辛く今までのほうがシンプルに感じるので、今後の改善に期待です。

## その他

- `createNativeStackNavigator` という、内部でネイティブのナビゲーションを使うナビゲーションが新しく追加されます
- 現在は `react-navigation-hooks` を通して提供されている、 `useNavigation` などの Hooks を標準で提供するようになります
- ひっそりと [Web Support](https://reactnavigation.org/docs/en/next/web-support.html) のページが追加されており、 今後 `react-native-web` に対応することで Expo の Web 対応を強化していく方向性になると予想されます


## まとめ

React Navigation v5 は [react-navigation/navigation-ex](https://github.com/react-navigation/navigation-ex/releases) リポジトリで開発中です。まだ alpha 段階で正式リリースまでは見たところ早くても数ヶ月はかかるかなという感じですし、 [v4 との互換機能](https://reactnavigation.org/docs/en/next/compatibility.html) も用意されているようなのでそこまでトラブルはさそうですが、このように変わるということだけでも知っておいて損はないかなと思いました。

今回手元で色々と試した内容を GitHub にプッシュしておりますので、興味があれば参考にしてみてください。
https://github.com/shinnoki/react-navigation-v5-example