企画
次のような画面構成でスクリーンを構築したい。
Bottom Tabs Navigator
にスクリーンの代わりにStack Navigator
を2つ配置して、それぞれのStack Navigator
に2つずつスクリーンを配置する感じ。
そして、それぞれのスクリーンから各ナビゲーションのスクリーンに遷移したい。
これを実現するためにReact Navigationの機能を使っていくが、ルート名とパラメータの型チェックを行いたいのでルート名とルートのパラメータのマッピングを持つオブジェクト型を作成する。 そのために、ルートにtypes.tsxを作成する。
作成する型はそれぞれ次の通り
RootStackParamList
Root
のBottom Tab Navigator
を配置するStack Navigator
用の型
RootTabParamList
TabOne
とTabTwo
のStack Navigator
を配置するBottom Tab Navigator
用の型
TabOneStackParamList
Alpha
とBeta
のスクリーンコンポーネントを配置するStack Navigator
用の型
TabTwoStackParamList
Gamma
とDelta
のスクリーンコンポーネントを配置するStack Navigator
用の型
TabOneStackScreenProps
TabOne
のStack Navigator
に配置されるスクリーンコンポーネントのProps
用の型
TabTwoStackScreenProps
TabTwo
のStack Navigator
に配置されるスクリーンコンポーネントのProps
用の型
TL;DR
最終的なプロジェクトはこちらから確認できます。
プロジェクトを作成する
Expo CLIを使ってReact Nativeのプロジェクトを準備する。
今回はテンプレートとしてexpo-template-blank-typescriptを使用する。
アプリ名は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
で参照されているRootTabParamList
はBottom 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
TabOne
やTabTwo
のStack Navigator
に配置したスクリーンコンポーネントで受け取ることができるProps
の型を定義するために、CompositeScreenProps
とBottomTabScreenProps
、そして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でエクスポートする
上記で定義したTabOneStackParamList
やTabTwoStackParamList
を使ってそれぞれのスクリーンの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> ); }
TabOne
とTabTwo
のStack 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> ); }
最後にTabOne
とTabTwo
という名前を担う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
からTabOneNavigator
のBetaScreen
、そしてTabTwoNavigator
のGammaScreen
、DeltaScreen
に遷移する。
BetaScreen
からTabOneNavigator
のAlphaScreen
、そしてTabTwoNavigator
のGammaScreen
、DeltaScreen
に遷移する。
GammaScreen
からTabOneNavigator
の AlphaScreen
とBetaScreen
、そしてTabTwoNavigator
のDeltaScreen
に遷移する。
DeltaScreen
からTabOneNavigator
の AlphaScreen
とBetaScreen
、そしてTabTwoNavigator
のGammaScreen
に遷移する。
同一ナビゲーターの兄弟スクリーンに遷移する場合は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", }, }); }, []);
まとめ
初見だと、どの型がどれに対応していて、遷移する場合はどんな引数を渡せば良いか理解しにくいかもしれないので、ぜひこの記事を読みながら少しでも理解の手助けになってくれると嬉しい。