🍣

【RN】StackとBottomTabNavigatorを組み合わせたサンプル

2021/07/20に公開1

はじめに

今回はReact Nativeでアプリを作る上基本となるStackNavigatorBottomTabNavigatorを組み合わせた画面構成のサンプルについて紹介していきます。
なお、コードはTypescriptで記載します。

用語解説

先に、今回の記事で紹介する各用語について紹介していきます。

StackNavigator

StackNavigatorは主にはアプリにおける「一覧画面」と「詳細画面」のような関係性がある画面間の遷移の用いられます。
特徴として画面が手前に積み重なっていくような挙動をし、iOSの右スワイプ(画面の左端を掴んで右にスワイプ)やAndroidの戻るボタンで1つ前の画面に戻ることができます。

BottomTabNavigator

アプリにおいて機能がいくつかのカテゴリに分かれている場合にBottomTabNavigatorは用いられます。
TwitterFacebookInstagramなど、多機能なアプリは大概このBottomTabNavigatorで画面制御を行っています。

インストール

今回はreact-navigationを使用していきます。

https://reactnavigation.org

react-navigationはメジャーバージョンごとに実装方法が大きく異なってきますが、ここでは記事執筆時点(2021/07/14)時点で最新バージョンであるv5系を基準としていきます。

ReactNativeのプロジェクトは作成済みのものとして、以下のライブラリを追加していきましょう。

yarn add @react-navigation/native react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view  @react-navigation/stack @react-navigation/bottom-tabs

package.jsondependenciesは以下のようになりました。

"@react-native-community/masked-view": "^0.1.11",
"@react-navigation/bottom-tabs": "^5.11.11",
"@react-navigation/native": "^5.9.4",
"@react-navigation/stack": "^5.14.5",
"react": "17.0.1",
"react-native": "0.64.1",
"react-native-gesture-handler": "^1.10.3",
"react-native-reanimated": "^2.2.0",
"react-native-safe-area-context": "^3.2.0",
"react-native-screens": "^3.4.0",

iOSの場合はpod installを行います。

cd ios && pod install

実装

それでは早速実装例に移りたいと思います。
完成イメージは以下の通りとなっています。

BottomTabNavigatorとして2画面、そしてそれぞれの画面から派生する画面として2画面をStackNavigatorに用意します。

ヘッダーの実装

先に、各画面で表示させるヘッダーを作成します。
実は後ほど実装するStackNavigatorの画面にはプロパティとしてヘッダーを指定することができます。
ただ、今回のようにBottomTabNavigatorを組み合わせて扱う場合、BottomTabNavigatorの中の画面によってヘッダの表示を分けたいケースが多いです。

そういった場合に、StackNavigatorのヘッダーだけではカスタムしにくい箇所が発生するため、今回はヘッダーを自作して使用しています。

構造はいたってシンプルで、「表示するタイトル」とヘッダーの左側、右側に表示する要素を親から指定できるようにしています。

Header.tsx
Header.tsx
import React from 'react';
import {View, Text} from 'react-native';

type Props = {
  /** ヘッダに表示するタイトル */
  title: string;
  /** ヘッダ左側の要素 */
  left?: React.ReactNode;
  /** ヘッダ右側の要素 */
  right?: React.ReactNode;
};

/**
 * オリジナルのヘッダコンポーネント
 * @param props
 * @returns
 */
const Header: React.VFC<Props> = props => {
  const {title, left, right} = props;
  return (
    <View
      style={{
        height: 60,
        flexDirection: 'row',
        alignItems: 'center',
      }}>
      <View style={{flex: 0.25}}>{left}</View>
      <Text
        style={{
          flex: 1,
          textAlign: 'center',
          alignItems: 'center',
        }}>
        {title ?? ''}
      </Text>
      <View style={{flex: 0.25}}>{right}</View>
    </View>
  );
};

export default Header;

TabNavigatorの実装

つづいてTabNavigator関連の画面を作ります。
といっても今回は先ほど作成したHeaderを使っていること以外は<Text>を表示するだけのシンプルな画面を2種類用意します。

Tab1.tsx
Tab1.tsx
import React, {ReactNode} from 'react';
import {useMemo} from 'react';
import {View, Text, TouchableOpacity} from 'react-native';
import {useNavigation} from '@react-navigation/native';
import Header from '../Common/Header';

/** Tab1 */
const Tab1: React.VFC = () => {
  const navigation = useNavigation();
  const right: ReactNode = useMemo(() => {
    return (
      <TouchableOpacity
        style={{padding: 10}}
        onPress={() => {
          navigation.navigate('Stack1');
        }}>
        <Text>Hoge</Text>
      </TouchableOpacity>
    );
  }, [navigation]);
  return (
    <View style={{flex: 1}}>
      <Header title="Tab1" right={right} />
      <View
        style={{
          flex: 1,
          justifyContent: 'center',
          alignContent: 'center',
          backgroundColor: '#c9ff93',
        }}>
        <Text style={{textAlign: 'center'}}>This Screen is Tab1.</Text>
      </View>
    </View>
  );
};

export default Tab1;

Tab2.tsx
Tab2.tsx
import React, {ReactNode} from 'react';
import {useMemo} from 'react';
import {View, Text, TouchableOpacity} from 'react-native';
import {useNavigation} from '@react-navigation/native';
import Header from '../Common/Header';

/** Tab1 */
const Tab2: React.VFC = () => {
  const navigation = useNavigation();
  const right: ReactNode = useMemo(() => {
    return (
      <TouchableOpacity
        style={{padding: 10}}
        onPress={() => {
          navigation.navigate('Stack2');
        }}>
        <Text>Fuga</Text>
      </TouchableOpacity>
    );
  }, [navigation]);
  return (
    <View style={{flex: 1}}>
      <Header title="Tab2" right={right} />
      <View
        style={{
          flex: 1,
          justifyContent: 'center',
          alignContent: 'center',
          backgroundColor: '#8ec6ff',
        }}>
        <Text style={{textAlign: 'center'}}>This Screen is Tab2.</Text>
      </View>
    </View>
  );
};

export default Tab2;

上記の2種類の画面を作成したらいよいよBottomTabNavigatorの作成です。
作成自体はcreateBottomTabNavigator()で行います。
作成したTabインスタンスにScreenとして2種類の画面を追加します。

TabNavigator.tsx
import React from 'react';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import Tab1 from './Tab1';
import Tab2 from './Tab2';

const Tab = createBottomTabNavigator();

/** TabNavigator */
const TabNavigator: React.VFC = () => {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Tab1" component={Tab1} />
      <Tab.Screen name="Tab2" component={Tab2} />
    </Tab.Navigator>
  );
};

export default TabNavigator;

StackNavigatorの実装

次にStackNavigatorの実装です。
まずはBottomTabNavigatorの時と同様に、配置する2画面を作成します。

Stack1.tsx
Stack1.tsx
import React, {ReactNode} from 'react';
import {useMemo} from 'react';
import {View, Text, TouchableOpacity} from 'react-native';
import {useNavigation} from '@react-navigation/native';
import Header from '../Common/Header';

/** Stack1 */
const Stack1: React.VFC = () => {
  const navigation = useNavigation();
  const left: ReactNode = useMemo(() => {
    return (
      <TouchableOpacity
        style={{padding: 10}}
        onPress={() => {
          navigation.goBack();
        }}>
        <Text>戻る</Text>
      </TouchableOpacity>
    );
  }, [navigation]);
  return (
    <View style={{flex: 1}}>
      <Header title="Stack1" left={left} />
      <View
        style={{
          flex: 1,
          justifyContent: 'center',
          alignContent: 'center',
          backgroundColor: '#ff93c9',
        }}>
        <Text style={{textAlign: 'center'}}>This Screen is Stack1.</Text>
      </View>
    </View>
  );
};

export default Stack1;

Stack2.tsx
Stack2.tsx
import React, {ReactNode} from 'react';
import {useMemo} from 'react';
import {View, Text, TouchableOpacity} from 'react-native';
import {useNavigation} from '@react-navigation/native';
import Header from '../Common/Header';

/** Stack2 */
const Stack2: React.VFC = () => {
  const navigation = useNavigation();
  const left: ReactNode = useMemo(() => {
    return (
      <TouchableOpacity
        style={{padding: 10}}
        onPress={() => {
          navigation.goBack();
        }}>
        <Text>戻る</Text>
      </TouchableOpacity>
    );
  }, [navigation]);
  return (
    <View style={{flex: 1}}>
      <Header title="Stack2" left={left} />
      <View
        style={{
          flex: 1,
          justifyContent: 'center',
          alignContent: 'center',
          backgroundColor: '#cc99ff',
        }}>
        <Text style={{textAlign: 'center'}}>This Screen is Stack2.</Text>
      </View>
    </View>
  );
};

export default Stack2;

画面の作成が終わったらStackNavigatorの作成です。
作成した2画面に加えて、先ほどのBottomTabNavigatorStack.Screenとして加えています。

StackNavigator.tsx
import React from 'react';
import {createStackNavigator} from '@react-navigation/stack';
import TabNavigator from '../BottomTab/TabNavigator';
import Stack1 from './Stack1';
import Stack2 from './Stack2';

const Stack = createStackNavigator();

/** TabNavigator */
const StackNavigator: React.VFC = () => {
  // screenOptionsでheaderShown:falseを指定して、デフォルトではヘッダを表示させない
  return (
    <Stack.Navigator screenOptions={{headerShown: false}}>
      <Stack.Screen name="Tab" component={TabNavigator} />
      <Stack.Screen name="Stack1" component={Stack1} />
      <Stack.Screen name="Stack2" component={Stack2} />
    </Stack.Navigator>
  );
};

export default StackNavigator;

アプリ全体に反映

最後に、今まで作成してきたものをアプリに反映させます。
StackNavigator.tsxApp.tsx内に配置しましょう。

react-navigationNavigatorを使う注意点として、必ずNavigationContainerの直下に1つだけNavigatorを配置するようにします。

App.tsx
import React from 'react';
import {SafeAreaView} from 'react-native';
import {NavigationContainer} from '@react-navigation/native';
import StackNavigator from './src/Stack/StackNavigator';

const App: React.VFC = () => {
  return (
    <SafeAreaView style={{flex: 1}}>
      <NavigationContainer>
        <StackNavigator />
      </NavigationContainer>
    </SafeAreaView>
  );
};

export default App;

これで冒頭のサンプルのように動作するようになりました。

まとめ

今回はStackNavigatorBottomTabNavigatorを使ったアプリの画面サンプルについてご紹介しました。
扱ったコード全体は私のリポジトリ内にも載せておきました。

https://github.com/ShutoYamada/rn-stack-and-bottom-navigator-sample-ts

ここにReduxやらFirebaseAWSといったバックエンドとの接続を加えて、実際のアプリ開発を進めていくイメージになります。

今回は画面の遷移をuseNavigation()を使って行っていますが、Reduxを使う場合は画面遷移のアクションをdispatchしていく形にする手もあります。

今回の内容が参考になれば幸いです。

Discussion

ikmzkroikmzkro

フッタータブの実装で非常に助かりました!ありがとうございました!