以下のようなアプリでは、上部のタブの部分と、リストの部分の2箇所で React Navigation が使われています。
「タブの中に Stack Navigation がある」というような Navigation が入れ子状態になっています。
ライブラリのインストール
タブのナビゲーションの作成部分は以下の記事にあります。
この記事では Tab 内の FlatList 内にある Stack Navigation を実装していきます。
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>();
をコンポーネント内で使うすべてのコンポーネントを設定しています。
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
というコンポーネントにしています。
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 に移動しています。
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 });
のように、型定義したパラメータを次のスクリーンに渡すことができます。
その、「ナビゲーションで渡すパラメータの型」は以下のように定義します。
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 の解説を参照してみてください。