React Native で画面遷移やタブメニュー【react-navigation 5.x】

# react-navigation とは
[react-navigation](https://reactnavigation.org/) とは、React Native アプリのルーティングで使える便利なライブラリです。
画面遷移や、タブバー、そしてドローワー（横からすっとでてくるメニュー）などを利用できるので非常に便利。
4.x->5.x のメジャーバージョンアップに伴い、コンポーネントベースになりました。

今回は、5.x の使い方を紹介します。

# インストール
```sh
# ライブラリのインストール
yarn add @react-navigation/native
# dependencies のインストール
yarn add react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view
```

### App.tsx

```tsx
import { NavigationContainer } from '@react-navigation/native';
import React, { useEffect } from 'react';
import HomeNavigator from './Home';
import NavigationService from './navigation-service';

const App: React.FunctionComponent = () => {
  return (
    <NavigationContainer>
      <HomeNavigator />
    </NavigationContainer>
  );
};

export default App;
```

これで準備は完了です。

# 画面遷移の実装
ライブラリをインストールします。

```
yarn add @react-navigation/stack
```

サンプルコードは以下のようになります。

```tsx
import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';
import { Alert, Button } from 'react-native';

const Stack = createStackNavigator();

const HomeNavigator = () => (
  <Stack.Navigator
    screenOptions={{
      headerTintColor: ...,
      headerStyle: {
        backgroundColor: ...,
      },
    }}
  >
    <Stack.Screen
      name="Home"
      component={Home}
      options={{
        headerRight: () => (
          <Button
            onPress={() => Alert.alert('This is a button!')}
            title="Info"
            color="#fff"
          />
        ),
      }}
    />
    <Stack.Screen name="Details" component={Details} />
  </Stack.Navigator>
);

...

export default HomeNavigator;

```

最初に `createStackNavigator` でスタックを定義しています。
作成したスタックは `Stack.Navigator`, `Stack.Screen` の形で使用します。

`screenOptions` には、このナビゲーター全体に適用するレイアウトを定義することができます。
個々のスクリーンに適用するレイアウトは `options` に記載します。

`component` で画面の要素を見ていきます。
二種類の書き方ができます。

```tsx
const Home: React.FunctionComponent = () => {
  const navigation = useNavigation();
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Button
        title="Go to Details"
        onPress={() => navigation.navigate('Details')}
      />
    </View>
  );
};
```

```tsx
const Home: React.FunctionComponent = ({ navigation }) => {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Button
        title="Go to Details"
        onPress={() => navigation.navigate('Details')}
      />
    </View>
  );
};
```

`navigation` を渡す方法は、 `react-navigation 4.x` までと変わりませんね。
`props` として渡す方法の型定義が面倒になってしまったため、私は Hooks の方を使ってます。

ボタンをクリックすると、 Details に遷移します。

# タブメニューの実装
ライブラリをインストールします。

```
yarn add @react-navigation/bottom-tabs
```

サンプルコードは以下のようになります。

```tsx
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import React, { useLayoutEffect } from 'react';
import Ionicons from 'react-native-vector-icons/Ionicons';

const Tab = createBottomTabNavigator();

const TabNavigator = ({ navigation, route }) => {
  useLayoutEffect(() => {
    navigation.setOptions({
      headerTitle: route.state
        ? route.state.routes[route.state.index].name
        : route.params?.screen || 'Home',
    });
  }, [navigation, route]);

  return (
    <Tab.Navigator
      screenOptions={({ route }) => ({
        tabBarIcon: ({ focused, color, size }) => {
          let iconName;

          if (route.name === 'Home') {
            iconName = focused
              ? 'ios-information-circle'
              : 'ios-information-circle-outline';
          } else if (route.name === 'Settings') {
            iconName = focused ? 'ios-list-box' : 'ios-list';
          }

          return <Ionicons name={iconName} size={size} color={color} />;
        },
      })}
      tabBarOptions={{
        activeTintColor: 'tomato',
        inactiveTintColor: 'gray',
      }}
    >
      <Tab.Screen name="Home" component={Home} />
      <Tab.Screen name="Settings" component={Settings} />
    </Tab.Navigator>
  );
};

...

export default TabNavigator;
```

`createBottomTabNavigator` でタブを作成。
`Tab.Navigator`, `Tab.Screen` で実際の内容を定義します。

`screenOptions`, `tabBarOptions` の代わりに、カスタムのタブメニューを利用することも可能です。 `tabBar={(props) => <TabBar {...props} />}` で定義します。
`TabBar` は以下のような形で書くことができます。

```tsx
import React from 'react';
import { Dimensions, Text, View } from 'react-native';
import { TouchableOpacity } from 'react-native-gesture-handler';

const { width: windowWidth } = Dimensions.get('window');

const TabBar = ({ state, descriptors, navigation }) => {
  const tabWidth = windowWidth / state.routes.length;
  return (
    <View style={{ flexDirection: 'row', width: windowWidth, height: 64 }}>
      {state.routes.map((route, index) => {
        const { options } = descriptors[route.key];
        const label =
          options.tabBarLabel !== undefined
            ? options.tabBarLabel
            : options.title !== undefined
            ? options.title
            : route.name;

        const isFocused = state.index === index;

        const onPress = () => {
          const event = navigation.emit({
            type: 'tabPress',
            target: route.key,
          });

          if (!isFocused && !event.defaultPrevented) {
            navigation.navigate(route.name);
          }
        };

        const onLongPress = () => {
          navigation.emit({
            type: 'tabLongPress',
            target: route.key,
          });
        };

        return (
          <TouchableOpacity
            key={route.key}
            accessibilityRole="button"
            accessibilityStates={isFocused ? ['selected'] : []}
            accessibilityLabel={options.tabBarAccessibilityLabel}
            onPress={onPress}
            onLongPress={onLongPress}
            style={{
              flex: 1,
              width: tabWidth,
              paddingTop: 10,
            }}
          >
            <Text
              style={{
                color: isFocused ? '#673ab7' : '#222',
                alignSelf: 'center',
              }}
            >
              {label}
            </Text>
          </TouchableOpacity>
        );
      })}
    </View>
  );
};

export default TabBar;
```

# スライドメニューの実装
ライブラリをインストールします。

```
yarn add @react-navigation/drawer
```

サンプルコードは以下のようになります。

```tsx
import { createDrawerNavigator } from '@react-navigation/drawer';
import React from 'react';

const Drawer = createDrawerNavigator();

const DrawerNavigator = () => {
  return (
    <Drawer.Navigator initialRouteName="Home">
      <Drawer.Screen name="Home" component={Home} />
      <Drawer.Screen name="Notifications" component={Notifications} />
    </Drawer.Navigator>
  );
};

...

export default DrawerNavigator;
```

# おまけ
スクリーンのなかで画面遷移をする場合、`props` か `useNavigation` から `navigation` をうけとって使用する方法を紹介しました。

では、自前の関数など、スクリーン以外で利用する場合はどうすれば良いでしょうか。

### navigation-service.ts

```typescript
import React from 'react';

export const isMountedRef = React.createRef();
export const navigationRef = React.createRef();

const navigate = (name: string, params?: any) => {
  if (isMountedRef.current && navigationRef.current) {
    navigationRef.current?.navigate(name, params);
  } else {
    console.log('Not mounted.');
  }
};

export default {
  navigate,
};
```

### App.tsx
```tsx
const App: React.FunctionComponent = () => {
  useEffect(() => {
    // TypeScript error が出る
    isMountedRef.current = true;
    return () => (isMountedRef.current = false);
  }, []);

  return (
    <NavigationContainer ref={navigationRef as any}>
      <HomeNavigator />
    </NavigationContainer>
  );
};
```

公式ドキュメントでも紹介されている方法ですが、 isMountedRef.current の更新はうまくできないようです。
GitHub にも [issue](https://github.com/react-navigation/react-navigation/issues/6879) がありました。

コンポーネントがマウントされていれば `navigationRef.current?.navigate(...);` は動作します。

これで自前の関数から、

```typescript
const myFunc = () => {
  NavigationService.navigate('...');
}
```

といった形で利用可能になります。

# おわりに
別プロジェクトでは 4.x からバージョンアップを試みており、 TypeScript 周りでかなり苦労しています。
ただし、 Hooks との相性も良さそうですし、新規でプロジェクトを始める場合は 5.x を使ってみても良いかもしれません。

型定義周りはもう少し調査が必要そうです。

記事の内容に誤りなどございましたらご指摘いただけると幸いです。