React Native + Expo に、ルート管理ライブラリreact-navigationを導入する

この記事は、「[【連載】初めてのReact Native + Expo開発環境構築入門](https://qiita.com/hitotch/items/5142fff638c7805d84d5)」の子記事です。環境などの条件は、親記事をご覧ください。

---

**この記事はReact Navigation V4を前提に書かれています。2020年10月時点最新のV5対応は、[こちら](https://kyokara.com/expo-navigation-mac/)の兄弟記事にお任せしました。**

　[前々回](https://qiita.com/hitotch/items/70bbf3e7a7f5f79dc265)で、画面上にメッセージが出るだけのアプリが作成できましたが、全く動きがありません。そこで今回は画面の移動（画面遷移）を実現する方法として、ルート管理ライブラリであるreact-navigationを導入します。

# react-navigationの基本

　React Nativeアプリは、Reactアプリと違ってURLベースのアプリケーションではないので、ReactのようなURLベースのルート管理（つまりグローバルのdocumentオブジェクトベースのルート管理）はできません。react-navigationでは、画面を親子兄弟の構造で設計しておいて、それをもとにnavigationオブジェクトを生成し、propsとして下位画面コンポーネントに渡して使うことで、ルート動作を実現していきます。

　とりあえず、どんなコードができるのか見てみたい！という方はこちら：[Our first navigate v3](https://snack.expo.io/@react-navigation/our-first-navigate-v3)
　※このサンプルのコードはreact-navigation公式ですが、古いバージョンを使っています。このまま最新のExpo + react-navigationで使うと動きません。

# react-navigationをHello World!プロジェクトにインストール

　[前々回](https://qiita.com/hitotch/items/70bbf3e7a7f5f79dc265)までに作ったHello World!プロジェクトをVS Codeで開きます。
　``Ctrl + @`` でTerminalを開いて、以下のコマンドで本体のreact-navigationと、画面履歴を管理するreact-navigation-stackをインストールします。

```
npm install react-navigation
```

```
npm install react-navigation-stack
```

　さらに、react-native-gesture-handler と react-native-reanimated をインストールします。今回はExpoを使っているので、互換性があるバージョンを選択するため以下のようにnpmではなくExpoのコマンドでインストールします。

```
expo install react-native-gesture-handler react-native-reanimated
```

# react-navigation-stack を使ってみる

　Webブラウザでは、``<a>``タグで示されたハイパーリンクをユーザーがクリックすると、画面がリンク先に遷移して、閲覧履歴にリンク先が追加されます。ここでユーザーが戻るボタンを押すと、元の画面に遷移して閲覧履歴から先のリンク先ページが削除（pop）されます。
　React Nativeではこういった閲覧履歴の概念が存在せず、react-navigation-stack はまさにこの概念を導入するためのものです。

　この動作を実現するには、画面を別個に認識することと、閲覧履歴を管理することが必要になります。それでは実際にHello World!に次の画面「Detail Screen」を作ってみましょう。Hello World!の下に、「Go to Details」ボタンを作って、ボタンを押したら「Detail Screen」に飛ぶようにします。

変更前

```App.js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

export default function App() {
  return (
    <View style={styles.container}>
      <Text>Open up App.js to start working on your app!</Text>
      <Text>Hello World!</Text>
    </View>
  );
}

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

変更後

```App.js
import React from 'react';
import { StyleSheet, Button, Text, View } from 'react-native';
import { createAppContainer } from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';

class HomeScreen extends React.Component {
  render(){
    return (
      <View style={styles.container}>
        <Text>Open up App.js to start working on your app!</Text>
        <Text>Hello World!</Text>
        <Button
          title="Go to Details"
          onPress={() => this.props.navigation.navigate('Details')}
        />
      </View>
    );
  }
}

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

class DetailsScreen extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text>Details Screen</Text>
      </View>
    );
  }
}

const RootStack = createStackNavigator(
  {
    Home: HomeScreen,
    Details: DetailsScreen,
  },
  {
    initialRouteName: 'Home',
  }
);

const AppContainer = createAppContainer(RootStack);

export default class App extends React.Component {
  render() {
    return <AppContainer />;
  }
}
```

変更内容を順に見ていきましょう。

## 必要なコンポーネントをimport

```App.js
import { StyleSheet, Button, Text, View } from 'react-native';
import { createAppContainer } from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';
```

``createAppContainer`` と ``createStackNavigator`` が使えるようにimportしています。ついでにButtonも使えるようにしています。

## 初期画面をコンポーネント化

```App.js
class HomeScreen extends React.Component {
  render(){
    return (
      <View style={styles.container}>
...
      </View>
    );
  }
}
```

　初期画面をreact-navigationに１つの「画面」として認識させるため、純粋な関数で表現されていた初期画面をHomeScreenと命名してReact Componentにコンポーネント化しています。これで、のちに出てくるcreateStackNavigator()に画面を渡すことができます。

## ボタンを設置

```App.js
        <Button
          title="Go to Details"
          onPress={() => this.props.navigation.navigate('Details')}
        />
```

　React Nativeなので、```<a>```ではなく```<Button>```コンポーネントを使ってリンクボタンを作ります。リンク先は、「Details」という名前の画面です。

## Details Screenの画面コンポーネントを作成

```App.js
class DetailsScreen extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text>Details Screen</Text>
      </View>
    );
  }
}
```

　初期画面同様にReact ComponentとしてDetails Screenの画面コンポーネントを作成します。このとき、コンポーネントの名前がDetailsではなくDetailsScreenであることに注意してください。先ほど初期画面(HomeScreen)ではDetailsに飛ぶようにButtonを作りました。ところがここで作ったコンポーネントの名前は違います。名前をつなぐ作業は、このあとのcreateStackNavigatorで行います。

## Stack Navigatorを作成する

```App.js
const RootStack = createStackNavigator(
  {
    Home: HomeScreen,
    Details: DetailsScreen,
  },
  {
    initialRouteName: 'Home',
  }
);
```

　Stack Navigatorは、先に説明したように画面を認識して閲覧履歴を管理するオブジェクトです。
　ここでは初期化情報を渡してStack Navigatorを作成しています。
　第1パラメータでは、画面を定義しています。ここではHomeScreenコンポーネントをHomeという名前の画面で、DetailsScreenコンポーネントをDetailsという名前の画面で、登録しています。
　第2パラメータでは、初期画面をHomeに設定しています。これにより、ナビゲーション動作が始まったらまずHome、つまりHomeScreenコンポーネントが表示されることになります。

## App本体としてStack Navigatorを登録

```App.js
const AppContainer = createAppContainer(RootStack);

export default class App extends React.Component {
  render() {
    return <AppContainer />;
  }
}
```

　export defaultすると、App.jsのメインを指定することになります。今回は先に作成したRootStackに画面全体を任せたいので、これをAppコンテナ化し、それを一番上のコンテナとして出力するように指定します。

# 実行確認

　それでは変更を保存し、実行してみましょう。

```
expo start
```

　モバイル実機からExpo Clientを実行すると、以下のようになります。

<img width="375" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/d521f2d1-553d-8584-5edf-2b4669e7a175.png">

　Go to Detailsをタップすると、次の画面に移ります。
<img width="375" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/3d867d21-4597-1b82-1948-f8ba6aad161c.png">

　画面移動にはちゃんとアニメーションもついていて、しかもなんと、自動的にBackボタンがついてますね！このBackボタン、もちろんちゃんと動作します。