React Navigation で Tab の中で入れ子になっている Stack Navigationを使う

以下のようなアプリでは、上部のタブの部分と、リストの部分の2箇所で React Navigation が使われています。

「タブの中に Stack Navigation がある」というような Navigation が入れ子状態になっています。

ライブラリのインストール

タブのナビゲーションの作成部分は以下の記事にあります。

この記事では Tab 内の FlatList 内にある Stack Navigation を実装していきます。

shell
npm i @react-navigation/stack

RootNavigator を作成

NavigationContainer で対象のナビゲーションをすべて囲みます。

TabNavigator の中に タブに関するナビゲーションを入れてしまいます。

それで、 <Stack.Screen name={'Tab'} component={TabNavigator} options={{ headerShown: false }} /> のように、 Stack.Screen のコンポーネントとして設定します。

Stack.Screen には、 const navigation = useNavigation<NoteListScreenNavigationProp>(); をコンポーネント内で使うすべてのコンポーネントを設定しています。

RootNavigator
import NoteDetailScreen from '../screens/NoteDetailScreen';
import NoteListScreen from '../screens/NoteListScreen';
import EmptyNoteView from '../uiParts/EmptyNoteView';
import NoteListFooter from '../uiParts/NoteListFooter';
import TabNavigator from '../uiParts/TabNavigator';

import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';

const Stack = createStackNavigator();
export default function RootNavigator() {
  const selectedNoteIdState = useRecoilValue(SelectedNoteIdState);

  return (
    <NavigationContainer>
      <View style={styles.spacer}></View>
      <Stack.Navigator>
        <Stack.Screen name={'Tab'} component={TabNavigator} options={{ headerShown: false }} />
        <Stack.Screen
          name={'NoteList'}
          component={NoteListScreen}
          options={{ headerShown: false }}
        />
        <Stack.Screen
          name={'NoteDetail'}
          component={NoteDetailScreen}
          options={({ navigation }) => ({
            headerRight: () => (
              <FontAwesome
                name="trash-o"
                size={24}
                color="gray"
                style={{ marginRight: 15 }}
                onPress={() => {
                  onNoteDetailDeleteIconPress(navigation);
                }}
              />
            ),
          })}
        />
        <Stack.Screen
          name={'NoteListFooter'}
          component={NoteListFooter}
          options={{ headerShown: false }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

const styles = StyleSheet.create({
  spacer: {
    marginTop: 60,
  },
});

Stack Navigator の公式の解説は以下です。

TabNavigation は以下のようになっています。タブを表示させていた部分をそのまま TabNavigator というコンポーネントにしています。

TabNavigator.tsx
import useTab from '../../hooks/useTab';
import { SelectedTabIdState } from '../../states/atoms/SelectedTabIdState';
import NoteListScreen from '../screens/NoteListScreen';
import TabAddScreen from '../screens/TabAddScreen';
import TabSettingScreen from '../screens/TabSettingScreen';
import { Entypo, SimpleLineIcons } from '@expo/vector-icons';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
import * as Haptics from 'expo-haptics';
import { ActivityIndicator, Text, View } from 'react-native';
import { useSetRecoilState } from 'recoil';

const Tab = createMaterialTopTabNavigator();

const ADD_TAB_SCREEN_NAME = 'addTabScreen';
const SETTING_TAB_SCREEN_NAME = 'settingTabScreen';
export default function TabNavigator() {
  const { tabs, isLoading } = useTab();
  const setSelectedTabIdState = useSetRecoilState(SelectedTabIdState);
  const setSelectedTabId = (tabId: string) => {
    setSelectedTabIdState(tabId);
  };
  const getInitialRouteName = () => {
    if (tabs.length === 0) {
      return ADD_TAB_SCREEN_NAME;
    }
    return tabs[0].name;
  };

  if (isLoading) {
    return (
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <ActivityIndicator size="large" />
      </View>
    );
  }
  return (
    <Tab.Navigator
      initialRouteName={getInitialRouteName()}
      screenOptions={{
        tabBarScrollEnabled: true,
        tabBarItemStyle: { maxWidth: 160 },
      }}
    >
      {tabs.map((tab) => (
        <Tab.Screen
          key={tab.id}
          name={tab.name}
          component={NoteListScreen}
          initialParams={{ tabId: tab.id }}
          options={{
            tabBarLabel: ({ focused, color }) => (
              <Text numberOfLines={1} ellipsizeMode="tail" style={{ color }}>
                {tab.name}
              </Text>
            ),
          }}
          listeners={{
            tabPress: async (e) => {
              setSelectedTabId(tab.id);
            },
          }}
        />
      ))}
      <Tab.Screen
        key={ADD_TAB_SCREEN_NAME}
        name={ADD_TAB_SCREEN_NAME}
        component={TabAddScreen}
        initialParams={{ tabId: 'addTabScreen' }}
        options={{
          tabBarLabel: ({ focused, color }) => <Entypo name="plus" size={24} color={color} />,
        }}
        listeners={{
          tabPress: async (e) => {
            await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
          },
        }}
      />
      <Tab.Screen
        key={SETTING_TAB_SCREEN_NAME}
        name={SETTING_TAB_SCREEN_NAME}
        component={TabSettingScreen}
        initialParams={{ tabId: SETTING_TAB_SCREEN_NAME }}
        options={{
          tabBarLabel: ({ focused, color }) => (
            <SimpleLineIcons
              name="settings"
              size={24}
              color="black"
              onPress={async () => {
                await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
              }}
            />
          ),
        }}
        listeners={{
          tabPress: async (e) => {
            await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
          },
        }}
      />
    </Tab.Navigator>
  );
}

あるスクリーンから別のスクリーンに navigation.navigate で移動する

以下のコードの navigation.navigate('NoteDetail', { note: item }); の部分で、 List から Detail に移動しています。

NoteListScreen.tsx
import { type Note } from '../../databases/entities/Tab';
import useNote from '../../hooks/useNote';
import { SelectedNoteIdState } from '../../states/atoms/SelectedNoteIdState';
import { type NoteListScreenNavigationProp } from '../../types';
import EmptyNoteView from '../uiParts/EmptyNoteView';
import NoteListFooter from '../uiParts/NoteListFooter';
import { AntDesign } from '@expo/vector-icons';
import { useNavigation } from '@react-navigation/native';
import {
  ActivityIndicator,
  FlatList,
  Pressable,
  SafeAreaView,
  StyleSheet,
  Text,
  View,
} from 'react-native';
import { useSetRecoilState } from 'recoil';

export default function NoteListScreen({ route }: any) {
  const { notes, isNoteLoading } = useNote();
  const setSelectedNoteIdState = useSetRecoilState(SelectedNoteIdState);

  const navigation = useNavigation<NoteListScreenNavigationProp>();

  if (isNoteLoading) {
    return (
      <View style={styles.centered}>
        <ActivityIndicator size="large" />
        <NoteListFooter />
      </View>
    );
  }

  if (notes.length === 0) {
    return <EmptyNoteView />;
  }

  const renderNote = ({ item }: { item: Note }) => {
    return (
      <Pressable
        onPress={() => {
          setSelectedNoteIdState(item.id);
          navigation.navigate('NoteDetail', { note: item });
        }}
        style={styles.rowItem}
      >
        <Text style={styles.rowTitle}>{item.title}</Text>
        <AntDesign name="right" size={24} style={styles.navigationIcon} color="gray" />
      </Pressable>
    );
  };

  return (
    <SafeAreaView style={styles.container}>
      <FlatList data={notes} keyExtractor={(item) => item.id} renderItem={renderNote} />
      <NoteListFooter />
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  rowItem: {
    flexDirection: 'row',
    height: 70,
    alignItems: 'center',
    justifyContent: 'space-between',
    backgroundColor: '#fff',
    borderWidth: 1,
    borderColor: '#ECF0F1',
  },
  rowTitle: {
    fontSize: 18,
    marginLeft: 12,
  },
  navigationIcon: {
    alignItems: 'flex-end',
    marginRight: 12,
  },
  centered: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

Navigation で渡すパラメーターに型を設定する

const navigation = useNavigation<NoteListScreenNavigationProp>(); の中の <NoteListScreenNavigationProp> を設定すると、 navigation.navigate('NoteDetail', { note: item }); のように、型定義したパラメータを次のスクリーンに渡すことができます。

その、「ナビゲーションで渡すパラメータの型」は以下のように定義します。

type.ts
import { type Note } from '../databases/entities/Tab';
import { type ParamListBase } from '@react-navigation/native';
import { type StackNavigationProp } from '@react-navigation/stack';

export interface NoteStackNavigatorParamList extends ParamListBase {
  NoteList: undefined;
  NoteDetail: { note: Note };
}

export type NoteListScreenNavigationProp = StackNavigationProp<
  NoteStackNavigatorParamList,
  'NoteDetails'
>;

Navigator をネストさせる機能とは

タブの中にある Screen 内で Stack Navigation を使おうとしています。その際は、 Nesting navigators という機能を使います。これまでの例でも Nesting Navigator の機能を使っていました。

より汎用的な解説は以下の公式ドキュメントの Nesting navigators の解説を参照してみてください。