React Native + Expo で Top Tabs Navigator (ヘッダーメニュー)を作る

React Native で Top Tabs Navigator と呼ばれる SmartNews のタブのようなビューを作ってみます。

React Navigation を使います。

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

shell
npm install @react-navigation/native @react-navigation/material-top-tabs react-native-tab-view
shell
npx expo install react-native-screens react-native-safe-area-context react-native-pager-view

最も基本的な画面を作る

TopTabNavigator.tsx
import MemoListScreen from '../screens/MemoListScreen';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';

const Tab = createMaterialTopTabNavigator();
export default function TopTabNavigator() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Home" component={MemoListScreen} />
      <Tab.Screen name="Settings" component={MemoListScreen} />
    </Tab.Navigator>
  );
}

App.tsx は以下のように書きます。

App.tsx
import TopTabNavigator from './lib/components/uiParts/TopTabNavigator';
import { initialize } from './lib/databases/typeorm.config';
import { DataSourceReady } from './lib/states/atoms/DataSourceReady';
import { NavigationContainer } from '@react-navigation/native';
import { useEffect } from 'react';
import { StyleSheet, View } from 'react-native';
import { RecoilRoot, useSetRecoilState } from 'recoil';

export default function App() {
  return (
    <RecoilRoot>
      <NavigationContainer>
        <View style={styles.spacer}></View>
        <TopTabNavigator />
        <DatabaseInitializer />
      </NavigationContainer>
    </RecoilRoot>
  );
}

function DatabaseInitializer() {
  const setDataSourceReady = useSetRecoilState(DataSourceReady);
  useEffect(() => {
    async function initializeDatabase() {
      try {
        await initialize();
        setDataSourceReady(true);
        console.log('[INFO]DataSource initialized');
      } catch (err) {
        console.log(err);
        console.log('[ERROR]DataSource failed to initialize');
        setDataSourceReady(false);
      }
    }

    initializeDatabase();
  }, []);
  return null;
}

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

シミュレーターを立ち上げると、以下のような画面が表示されます。

タブで表示しているコンポーネントに型付きでパラメータを渡す

MemoListScreen.tsx
import { type RootTabParamList } from '../uiParts/TopTabNavigator';
import { type RouteProp } from '@react-navigation/native';
import { SafeAreaView, Text } from 'react-native';

type MemoListScreenRouteProp = RouteProp<RootTabParamList, 'Home' | 'Settings'>;

interface Props {
  route: MemoListScreenRouteProp;
}
export default function MemoListScreen({ route }: Props) {
  console.log(route);
  const { tabId } = route.params;
  return (
    <SafeAreaView>
      <Text>MemoList - {tabId}</Text>
    </SafeAreaView>
  );
}

TopTabNavigator.tsx は以下のようになります。

TopTabNavigator.tsx
import MemoListScreen from '../screens/MemoListScreen';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';

export interface RootTabParamList {
  Home: { tabId: string };
  Settings: { tabId: string };
  [key: string]: { tabId: string } | undefined;
}

const Tab = createMaterialTopTabNavigator<RootTabParamList>();
export default function TopTabNavigator() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Home" component={MemoListScreen} initialParams={{ tabId: 'home' }} />
      <Tab.Screen
        name="Settings"
        component={MemoListScreen}
        initialParams={{ tabId: 'settings' }}
      />
    </Tab.Navigator>
  );
}

React Natigation でパラメータに型を設定する方法は以下のドキュメントが参考になります。

動的にタブの数を増やす方法

<Tab.Screen name="Home"name が一意でなければいけないため、 RootTabParamList でかっちりと型を定めるのは泣きながら諦めました。

TopTabNavigator.tsx
import MemoListScreen from '../screens/MemoListScreen';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';

const Tab = createMaterialTopTabNavigator();

interface TabScreenProp {
  name: string;
  tabId: string;
}

const tabScreenProps: TabScreenProp[] = [
  { name: 'TodoList', tabId: '123' },
  { name: 'Memo', tabId: '456' },
  { name: 'Tasks', tabId: '789' },
  { name: '買い物', tabId: '101112' },
  { name: '下書き', tabId: '131415' },
];
export default function TopTabNavigator() {
  return (
    <Tab.Navigator>
      {tabScreenProps.map((tabScreenProp) => (
        <Tab.Screen
          key={tabScreenProp.name}
          name={tabScreenProp.name}
          component={MemoListScreen}
          initialParams={{ tabId: tabScreenProp.tabId }}
        />
      ))}
    </Tab.Navigator>
  );
}

MemoListScreen.tsx は以下のようにします。 any を使ってしまっています。

MemoListScreen.tsx
import { SafeAreaView, Text } from 'react-native';

export default function MemoListScreen({ route }: any) {
  const { tabId } = route.params;
  return (
    <SafeAreaView>
      <Text>MemoList - {tabId}</Text>
    </SafeAreaView>
  );
}

上記の例の場合、以下のような画面が表示されます。

タブバーを横幅いっぱいでスクロールできるようにする

<Tab.Navigator screenOptions={{ tabBarScrollEnabled: true, を設定すれば、タブナビゲーションでスクロールできるようになります。ついでに長いタブ名は text-overflow: ellipsis のように、折りたたみができるようにしました。

TopTabNavigator.tsx
import MemoListScreen from '../screens/MemoListScreen';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
import { Text } from 'react-native';

const Tab = createMaterialTopTabNavigator();

interface TabScreenProp {
  name: string;
  tabId: string;
}

const tabScreenProps: TabScreenProp[] = [
  { name: 'TodoList', tabId: '123' },
  { name: 'Memo', tabId: '456' },
  { name: 'Tasks', tabId: '789' },
  { name: '買い物', tabId: '101112' },
  { name: '下書き', tabId: '131415' },
  {
    name: '長い長いタブの名前はどうなるのかな?',
    tabId: '131415',
  },
];
export default function TopTabNavigator() {
  return (
    <Tab.Navigator screenOptions={{ tabBarScrollEnabled: true, tabBarItemStyle: { width: 160 } }}>
      {tabScreenProps.map((tabScreenProp) => (
        <Tab.Screen
          key={tabScreenProp.name}
          name={tabScreenProp.name}
          component={MemoListScreen}
          initialParams={{ tabId: tabScreenProp.tabId }}
          options={{
            tabBarLabel: ({ focused, color }) => (
              <Text numberOfLines={1} ellipsizeMode="tail" style={{ color }}>
                {tabScreenProp.name}
              </Text>
            ),
          }}
        />
      ))}
    </Tab.Navigator>
  );
}

できあがったコンポーネントは以下のようになります。