たかぎとねこの忘備録

プログラミングに関する忘備録を自分用に残しときます。マサカリ怖い。

ReactNavigationで他のナビゲーターのスクリーンに移動したり型チェックを行いたい

企画

次のような画面構成でスクリーンを構築したい。

Bottom Tabs Navigatorにスクリーンの代わりにStack Navigatorを2つ配置して、それぞれのStack Navigatorに2つずつスクリーンを配置する感じ。

そして、それぞれのスクリーンから各ナビゲーションのスクリーンに遷移したい。

これを実現するためにReact Navigationの機能を使っていくが、ルート名とパラメータの型チェックを行いたいのでルート名とルートのパラメータのマッピングを持つオブジェクト型を作成する。 そのために、ルートにtypes.tsxを作成する。

作成する型はそれぞれ次の通り

  • RootStackParamList
    • RootBottom Tab Navigatorを配置するStack Navigator用の型
  • RootTabParamList
    • TabOneTabTwoStack Navigatorを配置するBottom Tab Navigator用の型
  • TabOneStackParamList
  • TabTwoStackParamList
  • TabOneStackScreenProps
  • TabTwoStackScreenProps

TL;DR

最終的なプロジェクトはこちらから確認できます。

github.com

プロジェクトを作成する

Expo CLIを使ってReact Nativeのプロジェクトを準備する。

今回はテンプレートとしてexpo-template-blank-typescriptを使用する。

github.com

アプリ名はexpo-minimum-appとかなんとかにしておく。

expo init --template expo-template-blank-typescript

必要なパッケージのインストール

今回の実装に必要なパッケージは次の通り。

  • @react-navigation/bottom-tabs
  • @react-navigation/native
  • @react-navigation/native-stack
yarn add @react-navigation/bottom-tabs @react-navigation/native-stack

次のパッケージはExpo CLIを使ってインストールする。

  • react-native-screens
  • react-native-safe-area-context
expo install react-native-screens react-native-safe-area-context

RootStackParamList

ナビゲーター(Stack Navigator)にネストされたナビゲーター(Bottom Tab Navigator)に含まれているスクリーンのパラメーターの型を定義するにはNavigatorScreenParamsを使用する。

// types.tsx
import { NativeStackScreenProps } from "@react-navigation/native-stack";
...

export type RootStackParamList = {
  Root: NavigatorScreenParams<RootTabParamList> | undefined;
};

RootTabParamList

RootStackParamListで参照されているRootTabParamListBottom Tab Navigatorであり、その内部でそれぞれのタブでStack Navigatorをネストしているので、ここでもNavigatorScreenParamsを使用する。

// types.tsx

...

export type RootTabParamList = {
  TabOne: NavigatorScreenParams<TabOneStackParamList>;
  TabTwo: NavigatorScreenParams<TabTwoStackParamList>;
};

TabOneStackParamListとTabTwoStackParamList

今回実装するスクリーンでは遷移時にパラメーターを必要としないでの、undefinedとして定義する。

// types.tsx

...

export type TabOneStackParamList = {
  Alpha: undefined;
  Beta: undefined;
};

export type TabTwoStackParamList = {
  Gamma: undefined;
  Delta: undefined;
};

TabOneStackScreenProps

TabOneTabTwoStack Navigatorに配置したスクリーンコンポーネントで受け取ることができるPropsの型を定義するために、CompositeScreenPropsBottomTabScreenProps、そしてNativeStackScreenPropsを使用する。

// types.tsx

...

export type TabOneStackScreenProps<Screen extends keyof TabOneStackParamList> =
  CompositeScreenProps<
    BottomTabScreenProps<TabOneStackParamList, Screen>,
    CompositeScreenProps<
      BottomTabScreenProps<RootTabParamList>,
      NativeStackScreenProps<RootStackParamList>
    >
  >;

export type TabTwoStackScreenProps<Screen extends keyof TabTwoStackParamList> =
  CompositeScreenProps<
    BottomTabScreenProps<TabTwoStackParamList, Screen>,
    CompositeScreenProps<
      BottomTabScreenProps<RootTabParamList>,
      NativeStackScreenProps<RootStackParamList>
    >
  >;
CompositeScreenProps

スタックナビゲーターの中にボトムタブナビゲーターがネストされているような状況の時に、複数のナビゲーターからの型を簡単に組み合わせるために使用するのがCompositeScreenPropsである。

最初のパラメーターはプライマリーナビゲーション型とよばれ、このPropsが渡されるスクリーンを管理するナビゲーターの型である。このナビゲーター型をBottomTabScreenPropsなどで指定する場合に、BottomTabScreenProps<TabOneStackParamList, Screen>のようにその第二引数にスクリーン名を渡す必要がある。

2つ目のパラメーターはそのセカンダリーナビゲーション型とよばれ、プリまりナビゲーション型をネストしている親ナビゲーターの型である。

上記のようにStack > Bottom Tab > Stackのように複数の親ナビゲーターにネストされている場合はセカンダリナビゲーション型もCompositeScreenPropsを使ってネストする必要がある。

スクリーン用のPropsをtypes.tsxでエクスポートする

上記で定義したTabOneStackParamListTabTwoStackParamListを使ってそれぞれのスクリーンのPropsをtypes.tsxで定義してエクスポートする。

...

// TabOne Screens

export type AlphaScreenProps = TabOneStackScreenProps<"Alpha">;
export type BetaScreenProps = TabOneStackScreenProps<"Beta">;

// TabTwo Screens

export type GammaScreenProps = TabTwoStackScreenProps<"Gamma">;
export type DeltaScreenProps = TabTwoStackScreenProps<"Delta">;

実際に使用するときはtypes.tsxからそれぞれの型をインポートして使用する。

// screens/TabOne/AlphaScreen.tsx
...
import { AlphaScreenProps as Props } from "../../types";

export function AlphaScreen({ navigation }: Props) {
  ...
}

実際に定義した型を使用してアプリを構築する

まずはnavigation/index.tsxを作成する。React Navigationの機能を使用するには、アプリ全体をNavigationContainerで一度だけ囲む必要があるので、その処理をここで行う。

// navigation/index.tsx

import { NavigationContainer } from '@react-navigation/native';
import { RootNavigator } from './RootNavigator';

export function Navigation() {
  return (
    <NavigationContainer>
      <RootNavigator />
    </NavigationContainer>
  )
}

Bottom Tabs NavigatorをネストするルートのStack Navigatorをnavigation/RootNavigator.tsxに実装する。

// navigation/RootNavigator.tsx

import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { RootStackParamList } from "../types";
import { BottomTabNavigator } from "./BottomTabNavigator";

const Stack = createNativeStackNavigator<RootStackParamList>();

export function RootNavigator() {
  return (
    <Stack.Navigator>
      <Stack.Screen
        name="Root"
        component={BottomTabNavigator}
        options={{ headerShown: false }}
      />
    </Stack.Navigator>
  );
}

TabOneTabTwoStack Navigatorを配置するためのBottom Tabs Navigatorを実装する。

// navigation/BottomTabNavigator.tsx

import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { RootTabParamList } from "../types";
import { TabOneNavigator } from "./TabOneNavigator";
import { TabTwoNavigator } from "./TabTwoNavigator";

const BottomTab = createBottomTabNavigator<RootTabParamList>();

export function BottomTabNavigator() {
  return (
    <BottomTab.Navigator initialRouteName="TabOne">
      <BottomTab.Screen name="TabOne" component={TabOneNavigator} />
      <BottomTab.Screen name="TabTwo" component={TabTwoNavigator} />
    </BottomTab.Navigator>
  );
}

最後にTabOneTabTwoという名前を担うStack Navigatorを実装する。まずはTabOneNavigatorから。

import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { TabOneStackParamList } from "../types";
import { AlphaScreen } from "../screens/TabOne/AlphaScreen";
import { BetaScreen } from "../screens/TabOne/BetaScreen";

const TabOneStack = createNativeStackNavigator<TabOneStackParamList>();

export function TabOneNavigator() {
  return (
    <TabOneStack.Navigator
      screenOptions={{
        headerTitleAlign: "center",
      }}
    >
      <TabOneStack.Screen
        name="Alpha"
        component={AlphaScreen}
      />
      <TabOneStack.Screen
        name="Beta"
        component={BetaScreen}
      />
    </TabOneStack.Navigator>
  )
}

次にTabTwoNavigatorを実装する。

import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { TabTwoStackParamList } from "../types";
import { GammaScreen } from "../screens/TabTwo/GammaScreen";
import { DeltaScreen } from "../screens/TabTwo/DeltaScreen";

const TabTwoStack = createNativeStackNavigator<TabTwoStackParamList>();

export function TabTwoNavigator() {
  return (
    <TabTwoStack.Navigator
      screenOptions={{
        headerTitleAlign: "center",
      }}
    >
      <TabTwoStack.Screen name="Gamma" component={GammaScreen} />
      <TabTwoStack.Screen name="Delta" component={DeltaScreen} />
    </TabTwoStack.Navigator>
  );
}

スクリーンでは、types.tsxで定義したそれぞれのスクリーン用の型をインポートして使用する。例としてAlphaScreenコンポーネントを実装してみる。その他のスクリーンも同じようにして実装する。

// screens/TabOne/AlphaScreen.tsx

import { useNavigation } from "@react-navigation/native";
import { Box, Center } from "native-base";
import { Button } from "native-base";
import { useCallback } from "react";
import { AlphaScreenProps as Props } from "../../types";

export function AlphaScreen({ navigation }: Props) {
  const nav = useNavigation();
  nav.navigate("Root", {
    screen: "TabOne",
    params: {
      screen: "Alpha",
    },
  });
  const handleNavigateToBetaButton = useCallback(() => {
    navigation.navigate("Beta");
  }, []);
  const handleNavigateToGammaButton = useCallback(() => {
    navigation.navigate("Root", {
      screen: "TabTwo",
      params: {
        screen: "Gamma",
      },
    });
  }, []);
  const handleNavigateToDeltaButton = useCallback(() => {
    navigation.navigate("Root", {
      screen: "TabTwo",
      params: {
        screen: "Delta",
      },
    });
  }, []);
  return (
    <Box flex={1}>
      <Center flex={1}>
        <Button onPress={handleNavigateToBetaButton} mb={4}>
          TabOneのBetaスクリーンに遷移します
        </Button>
        <Button
          onPress={handleNavigateToGammaButton}
          colorScheme="secondary"
          mb={4}
        >
          TabTwoのGammaスクリーンに遷移します
        </Button>
        <Button onPress={handleNavigateToDeltaButton} colorScheme="amber">
          TabTwoのDeltaスクリーンに遷移します
        </Button>
      </Center>
    </Box>
  );
}

navigation/index.tsxで実装したNavigationコンポーネントMain.tsxのMainコンポーネントレンダリングする。

// Main.tsx

import { StatusBar } from "expo-status-bar";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { Navigation } from "./navigation";

export default function Main() {
  return (
    <SafeAreaProvider>
      <Navigation />
      <StatusBar style="auto" />
    </SafeAreaProvider>
  );
}

そして、エントリーポイントであるApp.tsxでMain.tsxのMainコンポーネントをインポートしてレンダリングするようにする。

// App.tsx

import { StatusBar } from "expo-status-bar";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { Navigation } from "./navigation";

export default function Main() {
  return (
    <SafeAreaProvider>
      <Navigation />
      <StatusBar style="auto" />
    </SafeAreaProvider>
  );
}

実際の画面にするとこんな感じ。

AlphaScreenからTabOneNavigatorBetaScreen、そしてTabTwoNavigatorGammaScreenDeltaScreenに遷移する。

BetaScreenからTabOneNavigatorAlphaScreen、そしてTabTwoNavigatorGammaScreenDeltaScreenに遷移する。

GammaScreenからTabOneNavigatorAlphaScreenBetaScreen、そしてTabTwoNavigatorDeltaScreenに遷移する。

DeltaScreenからTabOneNavigatorAlphaScreenBetaScreen、そしてTabTwoNavigatorGammaScreenに遷移する。

同一ナビゲーターの兄弟スクリーンに遷移する場合はnavigation.navigateにスクリーン名を渡すだけで良い。

const handleNavigateToBetaButton = useCallback(() => {
    navigation.navigate("Beta");
  }, []);

同じBottom Tabs Navigatorに属する他のStack Navigatorのスクリーンに遷移する場合はscreenキーとparamsキーを組み合わせてオブジェクトを作成してnavigation.navigateに渡す。

const handleNavigateToGammaButton = useCallback(() => {
    navigation.navigate("Root", {
      screen: "TabTwo",
      params: {
        screen: "Gamma",
      },
    });
  }, []);

まとめ

初見だと、どの型がどれに対応していて、遷移する場合はどんな引数を渡せば良いか理解しにくいかもしれないので、ぜひこの記事を読みながら少しでも理解の手助けになってくれると嬉しい。