ExpoとReactNavigationのLinkの貼り方

# 最初に
最近ReactNativeでexpoを使用して、開発する案件が増えているような気がしています。
ReactNativeアプリで以下のようなことしたいと思ったことはないでしょうか？

* Webブラウザからリンクで直接アプリを開きたい。
* アプリにpush通知を送った際に、push通知をタップされると任意のアプリのページに飛ばしたい。

上記のことはアプリ内にリンクを貼ることで実現できます。
今回はReactNative(expo)でのアプリ開発で重要であるアプリ内でのLinkの貼り方について解説します。

# 今回の目的
ReactNativeの画面の移動の方法としては、色々とあると思いますが、今回はReactNativeが公式で推奨している画面の移動方法の[ReactNavigation](https://reactnavigation.org/)と[expo-linking](https://docs.expo.io/guides/linking/)というライブラリを使用してリンク移動できるようにします。

# 環境構築
```
$ expo init expo-tutorial
✔ Choose a template: › tabs (TypeScript)     several example screens and tabs using react-navigation and TypeScript
✔ Downloaded and extracted project files.

$ cd expo-tutorial
$ npm i
$ npm start
```

環境構築ですが、`expo init`を使用して新しいプロジェクトを作成します。
`Choose a template`で色々と選択できるのですが、この際に上から3番目の`tabs (TypeScript) `を選択してください。
このtemplateを選択することで、すでにReactNavigationの設定及びLinkingの設定がされています。
`npm start`を実行するとブラウザでexpoが立ち上がり、QRコードからアプリを実行できます。

`bottomTab`で`TabOne`から`TabTwo`へページの移動が移動できます。

# アプリの解説
公式がすでにLinkまで貼ったアプリを作成してくれているので、
このアプリはどのような構造になっているのか解説することでLinkについて理解していきます。

## Navigationの構造
まず、今回のNavigationの構造は以下のようになっています。

![expo2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/951917/02ad6974-ee1f-63f3-079a-31d0a1fa59c1.jpeg)

RootNavigationの中にBottomNavigationがあり、その中にさらにTabOneStackNavigation及びTabTwoStackNavigationが含まれています。
そしてそのTabOne 及び　TabTwo StackNavigationの下に初めてTabOne及びTabTwoScreenがあります。

そのため、ユーザがTabOneScreenにアクセスするには次のフローを辿ります。

RootNavigation → BottomNavigation → TabOneStackNavigaiton → TabOneScreen

実際のコードを確認したいと思います。
必要なコードだけ一部抜粋して記載します。
主にNavigationのcomponentとname引数に注目してください。
nameはnavigationでページ移動する際に指定するものです。
例えば今回の場合は、

```
navigation.navigate("TabOneScreen")
```
と記述するとTabOneScreenに移動できたりします。

> RootNavigation (navigation/index.ts)

```typescript
function RootNavigator() {
  return (
    <Stack.Navigator screenOptions={{ headerShown: false }}>
      <Stack.Screen name="Root" component={BottomTabNavigator} />
      <Stack.Screen name="NotFound" component={NotFoundScreen} options={{ title: 'Oops!' }} />
    </Stack.Navigator>
  );
}
```

RootNavigationで`BottomTabNavigation`と`NotFoundScreen`への分岐があります。
`BottomTabNavigation`のnameは`Root`
`NotFoundScreen`のnameは`NotFound`

> BottomNavigation (navigation/BottomTabNavigation.tsx)

```typescript
export default function BottomTabNavigator() {
  const colorScheme = useColorScheme();

  return (
    <BottomTab.Navigator
      initialRouteName="TabOne"
      tabBarOptions={{ activeTintColor: Colors[colorScheme].tint }}>
      <BottomTab.Screen
        name="TabOne"
        component={TabOneNavigator}
        options={{
          tabBarIcon: ({ color }) => <TabBarIcon name="ios-code" color={color} />,
        }}
      />
      <BottomTab.Screen
        name="TabTwo"
        component={TabTwoNavigator}
        options={{
          tabBarIcon: ({ color }) => <TabBarIcon name="ios-code" color={color} />,
        }}
      />
    </BottomTab.Navigator>
  );
}
```
次に`BottomNavigator`から`TabOneNavigator`と`TabTwoNavigator`へ分岐があります。
`TabOneNavigator`のnameは`TabOne`
`TabTwoNavigator`のnameは`TabTwo`
`initialRootName`に`TabOne`とあるので、defaultでBottomTabNavigatorまで到達した場合は、
`TabOneNavigator`に処理が渡ります。


> TabOneStackNavigation (navigation/BottomTabNavigation.tsx)

```typescript
function TabOneNavigator() {
  return (
    <TabOneStack.Navigator>
      <TabOneStack.Screen
        name="TabOneScreen"
        component={TabOneScreen}
        options={{ headerTitle: 'Tab One Title' }}
      />
    </TabOneStack.Navigator>
  );
}
```
最後に`TabOneNavigator`から`TabOneScreen`に処理が渡り、`TabOneScreen`が表示されます。
`TabOneScreen`のnameは`TabOneScreen`

> TabTwoStackNavigation (navigation/BottomTabNavigation.tsx)

```typescript
function TabTwoNavigator() {
  return (
    <TabTwoStack.Navigator>
      <TabTwoStack.Screen
        name="TabTwoScreen"
        component={TabTwoScreen}
        options={{ headerTitle: 'Tab Two Title' }}
      />
    </TabTwoStack.Navigator>
  );
}
```
TabOneNavigatorと同じ挙動を行います。


また、RootNavigationの中にはNotFoundScreenというものが存在しますが、
このScreenは普通にアプリを使用する分には、到達できないScreenとなっています。
詳しくはLinkのところで解説します。

## リンクの構造
次にリンクの構造について解説します。

リンクについての記載がされているのは次のファイルになります。

> navigation/LinkingConfiguration.ts

```typescript
import * as Linking from 'expo-linking';

export default {
  prefixes: [Linking.makeUrl('/')],
  config: {
    screens: {
      Root: {
        screens: {
          TabOne: {
            screens: {
              TabOneScreen: 'one',
            },
          },
          TabTwo: {
            screens: {
              TabTwoScreen: 'two',
            },
          },
        },
      },
      NotFound: '*',
    },
  },
};
```
`prefixes`というのはアプリまでのURL(カスタムスキーマなどを含む)を記述するのですが、
`Linking.makeUrl`で現在のアプリの`/`までのURLを自動で作成してくれます。
<font color=red> ※ Expo内で動作しているアプリやスタンドアロンで動いているアプリなど、環境によって、URLが変わってくるので、expoに頼って自動で作成するようにしましょう。</font>

`config`にリンクの内容を記載しています。
よく観察すると、リンクの内容はNavigationと類似していることがわかります。

> 最初の`screens`に`Root`と`NotFound`があり、
`Root`内の`screens`に`TabOne`と`TabTwo`があり、
`TabOne`内に`TabOneScreen`が記載されています。

Navigationにリンクを与える際は、Linkの内容とNavigationの内容が同期している必要があります。
`screens`にはNavigatorのcomponent名ではなくname名を記載します。

### pathの記載
`TabOneScreen:'one'`とありますが、これがpathです。
`Linking.makeUrl('/') + "one"`でアプリ内でアクセスできます。
Expoで動作している場合は、次のURLをアプリ内のブラウザに入力してみてください。
`exp://127.0.0.1:19001/--/one`
もしくは、ターミナルから以下のコマンドを実行でも動作確認できます。(実機では確認不可)

```bash
npx uri-scheme open exp://127.0.0.1:19001/--/one --ios #andoridの場合は--androidを実行
```

![expo3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/951917/0d1703e8-1342-f138-54d7-001efd16dbed.jpeg)

pathのネストをする際は次のように記載します。

```typescript
export default {
  prefixes: [Linking.makeUrl('/')],
  config: {
    screens: {
      Root: {
        path:"bottomTab",
        screens: {
          TabOne: {
            screens: {
              TabOneScreen: 'one',
            },
          },
          TabTwo: {
            screens: {
              TabTwoScreen: 'two',
            },
          },
        },
      },
      NotFound: '*',
    },
  },
};
```
次の場合は`exp://127.0.0.1:19001/--/bottom/one`でアクセスできます。

リンクからデータを渡したいなどは、今回は説明しません。
次の公式ページを参考にしてください。

https://reactnavigation.org/docs/configuring-links/
https://docs.expo.io/guides/linking/#passing-data-to-your-app-through-the

### リンク先が見つからない時

リンクで移動する場合は、リンク先が見つからない場合も考慮する必要があります。
そのための記述が`NotFound:'*'`です。
この記述を用意することで、リンク先が見つからなかった場合は、この`NotFound`が呼びだされ
`NotFoundScreen`が表示されます。

## Navigationにpathのconfigファイルを設定
上記で作成したPathを設定したファイルをNavigationに設定する必要があります。
それは以下のように`NavigationContainer`の`linking`に先程作成したファイルを指定することで実現できます。

> navigation/index.ts

```typescript
import LinkingConfiguration from './LinkingConfiguration'; //pathの設定ファイル
export default function Navigation({ colorScheme }: { colorScheme: ColorSchemeName }) {
  return (
    <NavigationContainer
      linking={LinkingConfiguration} // ここでNavigationに設定する
      theme={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
      <RootNavigator />
    </NavigationContainer>
  );
}
```

# 補足情報
ちょっと便利な情報を記載します。

## LinkingConfigurationの型定義
デフォルトで作成されたものでは型定義が聞いていないため、使いにくいので、
pathのconfigファイルにも型定義を与え挙げると次のようなものになります。
> navigation/LinkingConfiguration.ts

```typescript
import * as Linking from 'expo-linking';
import { LinkingOptions } from "@react-navigation/native";


const LinkingConfiguration:LinkingOptions =  {
  prefixes: [Linking.makeUrl('/')],
  config: {
    screens: {
      Root: {
        screens: {
          TabOne: {
            screens: {
              TabOneScreen: 'one',
            },
          },
          TabTwo: {
            screens: {
              TabTwoScreen: 'two',
            },
          },
        },
      },
      NotFound: '*',
    },
  },
};

export default LinkingConfiguration
```
> 

## notificationとの連携
通知をタップしたときに好きなscreenにジャンプさせたいと思うときは
以下のように設定すると実行できます。

```typescript
import * as Linking from 'expo-linking';
import { LinkingOptions } from "@react-navigation/native";


const LinkingConfiguration:LinkingOptions =  {
  prefixes: [Linking.makeUrl('/')],
  subscribe(listener) {
    const onReceiveURL = ({ url }: { url: string }) => {
      console.log(url);
      listener(url);
    };
    Linking.addEventListener("url", onReceiveURL);

    const notificationClickedListener = Notifications.addNotificationResponseReceivedListener(
      (response) => {
        /**
         * 通知をクリックした際の処理
         * dataのurlを取得する。
         * 例: data:{"url":"notification"}
         */
        const { url } = response.notification.request.content.data;
        if (typeof url === "string") {
          //redirectUrlを取得
          const redirectUrl = Linking.createURL(url);
          Linking.openURL(redirectUrl);
          return;
        }
        // rootにリダイレクト
        const redirectUrl = Linking.createURL("/");
        Linking.openURL(redirectUrl);
      }
    );
    return () => {
      /**
       * 後処理
       */
      Linking.removeEventListener("url", onReceiveURL);
      notificationClickedListener.remove();
    };
  },
  config: {
    screens: {
      Root: {
        screens: {
          TabOne: {
            screens: {
              TabOneScreen: 'one',
            },
          },
          TabTwo: {
            screens: {
              TabTwoScreen: 'two',
            },
          },
        },
      },
      NotFound: '*',
    },
  },
};

export default LinkingConfiguration
```

詳細については説明しませんが、push通知時に指定できる`data`にJSON形式で`{"url":"one"}`といった感じで入力すると
通知をタップした際に`TabOneScreen`に移動することができます。

詳細は以下をご覧ください。
https://docs.expo.io/versions/latest/sdk/notifications/#handling-push-notifications-with-react-navigation

通知のテストは以下から簡単にできます。tokenの発行が入ります。
https://expo.io/notifications
token発行について
https://docs.expo.io/versions/latest/sdk/notifications/#fetching-tokens-for-push-notifications