React Nativeアプリの画面遷移を制御する「ナビゲーション」は、ユーザー体験を左右する最重要パーツです。React Nativeには組み込みのルーティング機構がないため、画面間の移動にはサードパーティライブラリが必要になります。その事実上の標準が React Navigation です。

2024年11月にリリースされたv7では、Static APIの正式導入やスクリーンのプリロード機能など、開発体験とパフォーマンスの両面で大幅な改善が入りました。本記事ではReact Navigation v7を軸に、Navigator の種類と選定基準、TypeScriptで型安全に設計する手法、そしてExpo Routerとの使い分けまでを体系的にまとめます。

React Navigationの概要と主要パッケージ

React Navigationは、React NativeおよびWebアプリ向けのルーティング・ナビゲーションライブラリです。GitHub上のreact-navigation/react-navigationリポジトリで開発が進められており、npmでの週間ダウンロード数は数百万規模に達しています。

コアパッケージとNavigator一覧

React Navigationはモノレポ構成で、コアと各Navigatorが分離されています。

パッケージ名役割用途
@react-navigation/nativeコアランタイム全プロジェクトで必須
@react-navigation/native-stackNative Stack Navigator画面の積み重ね遷移(最も基本)
@react-navigation/bottom-tabsBottom Tab Navigator画面下部のタブバー
@react-navigation/material-top-tabsMaterial Top Tabs画面上部のスワイプ可能タブ
@react-navigation/drawerDrawer Navigatorサイドメニュー
react-native-screensネイティブ画面管理パフォーマンス向上(推奨)
react-native-safe-area-contextSafe Area対応ノッチ等の回避領域管理

v7以降、@react-navigation/stack(JavaScript実装のStack)よりも、ネイティブスレッドで遷移アニメーションを処理する @react-navigation/native-stack が推奨されます。後者は react-native-screens と連携し、メモリ消費とフレーム落ちの両方を低減します。

環境構築とインストール手順

前提条件

  • React Native 0.72.0 以上
  • Expo SDK 52 以上(Expoプロジェクトの場合)
  • TypeScript 5.0.0 以上(TypeScript使用時)

パッケージのインストール

Expoプロジェクトの場合は以下のコマンドを実行します。

npx expo install @react-navigation/native react-native-screens react-native-safe-area-context

bare React Nativeプロジェクトではnpmまたはyarnでインストール後、iOS向けにCocoaPodsの同期が必要です。

npm install @react-navigation/native react-native-screens react-native-safe-area-context
cd ios && pod install && cd ..

使用するNavigatorに応じて、追加パッケージをインストールします。

# Stack Navigator
npx expo install @react-navigation/native-stack

# Bottom Tab Navigator
npx expo install @react-navigation/bottom-tabs

# Drawer Navigator
npx expo install @react-navigation/drawer react-native-gesture-handler react-native-reanimated

Android固有の設定

Android環境では MainActivityreact-native-screens の設定が必要です。MainActivity.kt(または .java)で RNScreensFragmentFactory を有効にしてください。

// MainActivity.kt
import com.swmansion.rnscreens.RNScreensFragmentFactory

class MainActivity : ReactActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        defaultFragmentFactory = RNScreensFragmentFactory()
        super.onCreate(null)
    }
}

3種類のNavigatorを理解する

React Navigationには用途に応じた複数のNavigatorがあります。ここでは頻繁に使われる3種類を取り上げます。

Native Stack Navigator — 画面積み上げ型の遷移

Stack Navigatorは、画面を「スタック(積み重ね)」構造で管理します。新しい画面に遷移するとスタックの上に追加され、「戻る」操作で最上位の画面が取り除かれます。iOSの標準的な右スライドイン・Androidのフェードアニメーションが自動適用されます。

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

type RootStackParamList = {
  Home: undefined;
  Detail: { itemId: number };
};

const Stack = createNativeStackNavigator<RootStackParamList>();

function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        <Stack.Screen
          name="Home"
          component={HomeScreen}
          options={{ title: 'ホーム' }}
        />
        <Stack.Screen
          name="Detail"
          component={DetailScreen}
          options={{ title: '詳細' }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

画面間でパラメータを渡すには navigation.navigate の第2引数にオブジェクトを指定します。

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

type HomeProps = NativeStackScreenProps<RootStackParamList, 'Home'>;

function HomeScreen({ navigation }: HomeProps) {
  return (
    <Button
      title="詳細を開く"
      onPress={() => navigation.navigate('Detail', { itemId: 42 })}
    />
  );
}

type DetailProps = NativeStackScreenProps<RootStackParamList, 'Detail'>;

function DetailScreen({ route }: DetailProps) {
  const { itemId } = route.params;
  return <Text>アイテムID: {itemId}</Text>;
}

Bottom Tab Navigator — 下部タブバー

Bottom Tab Navigatorは、画面下部にタブバーを配置し、タップで画面を切り替えます。SNSアプリやECアプリで広く使われるUIパターンです。

import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Ionicons from '@expo/vector-icons/Ionicons';

type TabParamList = {
  Feed: undefined;
  Search: undefined;
  Profile: undefined;
};

const Tab = createBottomTabNavigator<TabParamList>();

function TabNavigator() {
  return (
    <Tab.Navigator
      screenOptions={({ route }) => ({
        tabBarIcon: ({ focused, color, size }) => {
          const iconName = route.name === 'Feed'
            ? (focused ? 'home' : 'home-outline')
            : route.name === 'Search'
            ? (focused ? 'search' : 'search-outline')
            : (focused ? 'person' : 'person-outline');
          return <Ionicons name={iconName} size={size} color={color} />;
        },
      })}
    >
      <Tab.Screen name="Feed" component={FeedScreen} options={{ title: 'フィード' }} />
      <Tab.Screen name="Search" component={SearchScreen} options={{ title: '検索' }} />
      <Tab.Screen name="Profile" component={ProfileScreen} options={{ title: 'プロフィール' }} />
    </Tab.Navigator>
  );
}

v7では tabBarPosition オプションで 'left''right' を指定すると、タブレット向けにサイドバーレイアウトへ切り替えられます。さらに、タブ切り替え時のアニメーションもサポートされました。

Drawer Navigator — サイドメニュー

Drawer Navigatorは、画面の左端(または右端)からスワイプで引き出すサイドメニューを提供します。設定画面やカテゴリ一覧など、メインUIに常時表示する必要がないナビゲーションに適しています。

import { createDrawerNavigator } from '@react-navigation/drawer';

type DrawerParamList = {
  Dashboard: undefined;
  Settings: undefined;
  Help: undefined;
};

const Drawer = createDrawerNavigator<DrawerParamList>();

function DrawerNavigator() {
  return (
    <Drawer.Navigator initialRouteName="Dashboard">
      <Drawer.Screen name="Dashboard" component={DashboardScreen} options={{ title: 'ダッシュボード' }} />
      <Drawer.Screen name="Settings" component={SettingsScreen} options={{ title: '設定' }} />
      <Drawer.Screen name="Help" component={HelpScreen} options={{ title: 'ヘルプ' }} />
    </Drawer.Navigator>
  );
}

Drawer Navigatorは react-native-gesture-handlerreact-native-reanimated に依存するため、インストール時に両パッケージを含める必要があります。

v7の新機能 — Static APIとDynamic API

React Navigation v7最大の目玉機能が Static API です。v6まではDynamic API(JSXベースの設定)のみでしたが、v7ではオブジェクトベースの宣言的な設定方法が追加されました。

Dynamic API(従来方式)

const Stack = createNativeStackNavigator<RootStackParamList>();

function Navigation() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Detail" component={DetailScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

Static API(v7の推奨方式)

import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createStaticNavigation } from '@react-navigation/native';

const RootStack = createNativeStackNavigator({
  screens: {
    Home: {
      screen: HomeScreen,
      options: { title: 'ホーム' },
    },
    Detail: {
      screen: DetailScreen,
      options: { title: '詳細' },
    },
  },
});

const Navigation = createStaticNavigation(RootStack);

function App() {
  return <Navigation />;
}

Static APIの利点

観点Dynamic APIStatic API
TypeScript型推論ParamListの手動定義が必要スクリーン定義から自動推論
Deep Link設定linking propに別途パス定義スクリーン定義内に linking を同居
ボイラープレートNavigationContainer + JSXツリーcreateStaticNavigation のみ
実行時の柔軟性高い(条件分岐で画面出し分け可能)制限あり(ifフックによる条件分岐が必要)

Static APIでは、ParamListの型定義が不要になり、TypeScriptの推論だけで navigation.navigate('Detail', { itemId: 42 }) の型チェックが効きます。Deep Linkのパス設定も各スクリーン定義と同じ場所に書けるため、設定が分散しにくくなります。

一方、認証状態によるスクリーン出し分けなど実行時の条件分岐が多い場合は、Dynamic APIの方が柔軟です。両者は1つのプロジェクト内で混在させることも可能です。

TypeScriptで型安全なナビゲーションを設計する

React Navigationは TypeScript と親和性が高く、画面パラメータの型チェックを静的に行えます。ここでは Dynamic API を使う場合の型安全設計パターンを整理します。

ParamListの定義

// types/navigation.ts
export type RootStackParamList = {
  Home: undefined;
  Detail: { itemId: number; title?: string };
  Modal: { message: string };
};

export type MainTabParamList = {
  Feed: undefined;
  Search: { query?: string };
  Profile: { userId: string };
};

undefined を指定した画面はパラメータ不要、オブジェクト型を指定した画面は遷移時にそのパラメータが必須(?付きはオプション)になります。

ScreenPropsの型付け

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

type DetailScreenProps = NativeStackScreenProps<RootStackParamList, 'Detail'>;

function DetailScreen({ route, navigation }: DetailScreenProps) {
  // route.params.itemId は number 型
  // route.params.title は string | undefined 型
  // navigation.navigate('Home') は OK
  // navigation.navigate('Unknown') は コンパイルエラー
}

useNavigationフックの型付け

コンポーネントツリー深部で navigation propが渡されない場合、useNavigation フックで取得します。

import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';

type NavigationProp = NativeStackNavigationProp<RootStackParamList>;

function SomeButton() {
  const navigation = useNavigation<NavigationProp>();
  return (
    <Button
      title="詳細へ"
      onPress={() => navigation.navigate('Detail', { itemId: 1 })}
    />
  );
}

ネストしたNavigatorの型定義

TabNavigatorの中にStackNavigatorをネストするケースでは NavigatorScreenParams で親子の型をつなぎます。

import { NavigatorScreenParams } from '@react-navigation/native';

type RootStackParamList = {
  MainTabs: NavigatorScreenParams<MainTabParamList>;
  Modal: { message: string };
};

type MainTabParamList = {
  Feed: undefined;
  Profile: { userId: string };
};

この定義により、navigation.navigate('MainTabs', { screen: 'Profile', params: { userId: 'abc' } }) と書いたときに、ネスト先のパラメータまで型チェックが有効になります。

実務のアプリでは、複数のNavigatorを組み合わせて使います。代表的なネストパターンを3つ示します。

パターン1: Stack + Bottom Tabs

最も一般的な構成です。タブバーで主要画面を切り替え、各タブ内でStackによるドリルダウン遷移を行います。

RootStack
├── MainTabs (Bottom Tab Navigator)
│   ├── FeedStack (Native Stack)
│   │   ├── FeedList
│   │   └── FeedDetail
│   ├── SearchStack (Native Stack)
│   │   ├── SearchTop
│   │   └── SearchResult
│   └── ProfileStack (Native Stack)
│       ├── ProfileTop
│       └── EditProfile
└── Modal (presentation: 'modal')

パターン2: 認証フローの分離

ログイン状態に応じて表示するNavigatorを切り替えるパターンです。

function RootNavigation() {
  const { isAuthenticated } = useAuth();

  return (
    <NavigationContainer>
      <Stack.Navigator screenOptions={{ headerShown: false }}>
        {isAuthenticated ? (
          <Stack.Screen name="Main" component={MainTabs} />
        ) : (
          <Stack.Screen name="Auth" component={AuthStack} />
        )}
      </Stack.Navigator>
    </NavigationContainer>
  );
}

isAuthenticated が変化するとReact Navigationが自動的に遷移アニメーション付きで画面を切り替えます。navigation.navigate('Login') のような命令的な呼び出しは不要です。

パターン3: Drawer + Tabs + Stack

管理画面やダッシュボード系アプリで使われる3階層構成です。

Drawer Navigator
├── HomeTabs (Bottom Tab Navigator)
│   ├── DashboardStack
│   └── ReportsStack
├── Settings
└── Help

この場合、Drawerが最外層でTabsが中間層、Stackが最内層になります。Navigatorをネストするときは、画面のヘッダーが二重に表示されないよう headerShown: false を適切に設定してください。

Expo Routerとの比較と選定基準

Expo RouterはReact Navigationの上に構築されたファイルベースルーティングシステムです。Next.jsやNuxt.jsのように、app/ ディレクトリのファイル構造がそのままルート定義になります。

比較項目React NavigationExpo Router
ルーティング方式コードベース(JSX/オブジェクト)ファイルベース(ディレクトリ構造)
設定の自由度非常に高い規約に従う必要あり
Deep Link手動設定が必要ファイルパスから自動生成
Web対応部分的SSR・SSGを含むフル対応
TypeScriptv7で大幅改善ルート型の自動生成
学習コスト中程度Next.js経験者は低い
Expo依存なし(bare RNでも利用可)Expoプロジェクト必須
複雑なネスト柔軟に対応レイアウトファイルで対応

React Navigationが適するケース

  • bare React Nativeプロジェクト(Expoを使わない)
  • 既存のReact Navigation v6アプリからのアップグレード
  • 複雑な条件分岐を含むナビゲーションロジック
  • Web対応が不要、またはモバイル最優先のアプリ

Expo Routerが適するケース

  • 新規のExpoプロジェクト
  • WebとモバイルでURLを統一したいユニバーサルアプリ
  • Deep Linkの設定を自動化したい
  • Next.js的なファイルベースルーティングに慣れている

両者は排他的な選択肢ではなく、Expo Router内部ではReact Navigationが動作しています。Expo Routerを選んだ場合でも、@react-navigation/native-stack などのNavigatorの知識はそのまま活用できます。

パフォーマンス最適化のポイント

react-native-screensの有効活用

react-native-screens はスタック上の画面をネイティブのFragment/UIViewControllerとして管理します。表示されていない画面のビューツリーをネイティブ側で自動的にデタッチするため、複雑なスタック構成でもメモリ使用量が安定します。React Navigation v7では react-native-screens がデフォルト依存に含まれています。

Bottom TabsのLazy Loading

Bottom Tab Navigatorの各タブは、デフォルトでは初回レンダリング時に全タブの画面コンポーネントを生成します。lazy オプションを有効にすると、ユーザーがタブをタップするまでコンポーネントの生成を遅延できます。

<Tab.Navigator
  screenOptions={{
    lazy: true,
    freezeOnBlur: true,
  }}
>
  {/* ... */}
</Tab.Navigator>

freezeOnBlurreact-native-screens の機能で、非アクティブなタブの再レンダリングを抑制します。大量のデータを表示するリスト画面や地図画面を含むアプリで顕著な効果を発揮します。

画面プリロード(v7新機能)

v7ではスクリーンのプリロードが可能になりました。ユーザーが遷移する前にバックグラウンドで画面コンポーネントを準備しておくことで、遷移時の体感速度を向上させます。

const navigation = useNavigation();

// ユーザーがボタンをホバー/長押しした時点でプリロード
function handlePressIn() {
  navigation.preload('Detail', { itemId: 42 });
}

beforeRemoveによる未保存データの保護

フォーム入力中に誤って「戻る」操作をした場合に、確認ダイアログを表示して画面離脱を防止できます。

import { useNavigation } from '@react-navigation/native';
import { Alert } from 'react-native';

function EditScreen() {
  const navigation = useNavigation();
  const [hasUnsaved, setHasUnsaved] = React.useState(false);

  React.useEffect(() => {
    if (!hasUnsaved) return;

    const unsubscribe = navigation.addListener('beforeRemove', (e) => {
      e.preventDefault();
      Alert.alert(
        '変更を破棄しますか?',
        '保存していない変更があります。',
        [
          { text: '編集を続ける', style: 'cancel' },
          {
            text: '破棄する',
            style: 'destructive',
            onPress: () => navigation.dispatch(e.data.action),
          },
        ]
      );
    });

    return unsubscribe;
  }, [hasUnsaved, navigation]);

  // フォームUI ...
}

ヘッダーとスタイリングのカスタマイズ

ヘッダーの基本設定

options propまたは screenOptions でヘッダーの見た目を制御します。

<Stack.Screen
  name="Home"
  component={HomeScreen}
  options={{
    title: 'マイアプリ',
    headerStyle: { backgroundColor: '#6366f1' },
    headerTintColor: '#fff',
    headerTitleStyle: { fontWeight: 'bold' },
  }}
/>

ヘッダーを完全にカスタマイズ

デフォルトのヘッダーでは要件を満たせない場合、header オプションで独自コンポーネントを差し込めます。

<Stack.Screen
  name="Home"
  component={HomeScreen}
  options={{
    header: ({ navigation, route, options }) => (
      <CustomHeader title={options.title ?? route.name} />
    ),
  }}
/>

v7のSearchbar機能

v7からは全Navigatorのヘッダーに検索バーを組み込めます。

<Stack.Screen
  name="Search"
  component={SearchScreen}
  options={{
    headerSearchBarOptions: {
      placeholder: 'キーワードで検索',
      onChangeText: (event) => {
        // フィルタリング処理
      },
    },
  }}
/>

Deep Linkの設定方法

Deep Linkを使うと、URLスキーム(myapp://detail/42)やユニバーサルリンク(https://example.com/detail/42)からアプリ内の特定画面を直接開けます。

Dynamic APIでのDeep Link設定

const linking = {
  prefixes: ['myapp://', 'https://example.com'],
  config: {
    screens: {
      Home: '',
      Detail: 'detail/:itemId',
      MainTabs: {
        screens: {
          Feed: 'feed',
          Profile: 'profile/:userId',
        },
      },
    },
  },
};

function App() {
  return (
    <NavigationContainer linking={linking}>
      {/* Navigator定義 */}
    </NavigationContainer>
  );
}

Static APIでのDeep Link設定

Static APIでは各スクリーン定義内に linking を記述するため、パス設定がNavigator構成と一体化します。

const RootStack = createNativeStackNavigator({
  screens: {
    Home: {
      screen: HomeScreen,
      linking: '',
    },
    Detail: {
      screen: DetailScreen,
      linking: 'detail/:itemId',
    },
  },
});

よくあるトラブルと対処法

画面間でパラメータが undefined になる

TypeScriptでParamListを定義していても、実行時にパラメータが undefined になるケースがあります。主な原因は以下のとおりです。

  • navigation.navigate でパラメータを渡し忘れている
  • ネストしたNavigator間で screen / params の指定が漏れている
  • Deep Linkのパス定義と画面パラメータの型が不一致

ParamListでオプショナル(?)でないパラメータを定義している画面に対しては、必ず値を渡すようにしてください。

ヘッダーが二重に表示される

Navigatorをネストすると、外側と内側の両方がヘッダーを描画して二重表示になることがあります。外側のNavigatorの当該Screenに headerShown: false を指定して抑制します。

<Stack.Screen
  name="MainTabs"
  component={TabNavigator}
  options={{ headerShown: false }}
/>

Drawerのジェスチャーが反応しない

Drawer Navigatorは react-native-gesture-handler に依存します。エントリポイント(index.jsApp.tsx)の先頭で以下のインポートを追加してください。

import 'react-native-gesture-handler';

Expo SDK 52以降では react-native-gesture-handler がプリインストールされていますが、bare RNプロジェクトではリンク設定が漏れやすいポイントです。

まとめ

React Navigation v7は、Static APIの導入によるTypeScript体験の改善、画面プリロードによるパフォーマンス向上、ヘッダー検索バーやタブアニメーションなど、モバイルアプリに求められる機能を着実にカバーしています。

Navigator選定の目安として、画面を順序立てて掘り下げる場面にはNative Stackを、主要機能を横並びで提示したい場合はBottom Tabsを、補助的なメニューにはDrawerを割り当ててください。3種類を組み合わせる場合は、ヘッダーの二重表示やジェスチャーの競合に注意が必要です。

Expo Routerとの選択は、プロジェクトのExpo依存度とWeb対応の要否で判断します。Expoを使うならExpo Routerのファイルベースルーティングは強力な選択肢ですし、bare RNやモバイル特化のプロジェクトではReact Navigationの柔軟性が活きます。

公式ドキュメント(reactnavigation.org)にはインタラクティブな例やアップグレードガイドが充実しているため、実装時のリファレンスとして活用してください。