React Nativeアプリの画面遷移を実装するとき、ルーティングの設計で手が止まった経験はないでしょうか。Next.jsやNuxtで定番のファイルベースルーティングをモバイルアプリでも実現するのがExpo Routerです。app/ディレクトリにファイルを配置するだけでルートが自動生成され、iOS・Android・Webを同一コードで動かせます。

Expo Routerの概要

Expo Routerは、Expoフレームワークに組み込まれたルーティングライブラリです。内部ではReact Navigationを基盤として動作しており、React Navigationが提供するStackやTabsなどのナビゲーション機構を、ファイルシステムベースのルーティングとして抽象化しています。

ファイルベースルーティングの仕組み

app/ディレクトリ内のファイル構成がそのままルーティング定義になります。

app/
├── _layout.tsx       # ルートレイアウト
├── index.tsx         # / (ホーム画面)
├── about.tsx         # /about
├── settings/
│   ├── _layout.tsx   # settings配下のレイアウト
│   ├── index.tsx     # /settings
│   └── profile.tsx   # /settings/profile
└── users/
    └── [id].tsx      # /users/:id (動的ルート)

_layout.tsxはページとしてレンダリングされず、配下のルートに共通するナビゲーション構造(Stack、Tabsなど)を定義するためのファイルです。Webフレームワークにおけるレイアウトコンポーネントと同じ役割を果たします。

主な特徴

項目内容
ルーティング方式ファイルシステムベース(app/ディレクトリ)
対応プラットフォームiOS / Android / Web
基盤ライブラリReact Navigation
TypeScriptTyped Routesによる型安全な画面遷移
Deep Linkingルート定義に基づいて自動設定
SSG / SSRWebプラットフォームで静的エクスポート対応

セットアップ手順

新規プロジェクトの場合

Expoの最新テンプレートにはExpo Routerがデフォルトで含まれています。

npx create-expo-app@latest my-app
cd my-app
npx expo start

テンプレートから作成したプロジェクトにはapp/ディレクトリが用意されており、すぐにファイルベースルーティングを利用できます。

既存プロジェクトへの追加

既存のExpoプロジェクトにExpo Routerを導入する場合は、以下のコマンドでインストールします。

npx expo install expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar

app.json(またはapp.config.js)に設定を追加します。

{
  "expo": {
    "scheme": "myapp",
    "plugins": ["expo-router"]
  }
}

エントリーポイントをpackage.jsonで指定します。

{
  "main": "expo-router/entry"
}

ディレクトリ設計パターン

ルートグループによる構造化

丸括弧で囲んだディレクトリ名はURLパスに含まれません。ナビゲーション構造を整理する目的で使用します。

app/
├── _layout.tsx
├── (tabs)/
│   ├── _layout.tsx      # Tabs定義
│   ├── index.tsx         # / (タブ1)
│   └── explore.tsx       # /explore (タブ2)
├── (auth)/
│   ├── _layout.tsx       # 認証画面用レイアウト
│   ├── login.tsx         # /login
│   └── register.tsx      # /register
└── +not-found.tsx        # 404画面

(tabs)ディレクトリ内のindex.tsx/(tabs)/indexではなく/としてアクセスされます。ルートグループはナビゲーション階層の整理に不可欠なパターンです。

特殊ファイル一覧

ファイル名役割
_layout.tsxディレクトリ配下のレイアウト定義
index.tsxそのディレクトリのデフォルトルート
[param].tsx動的ルートパラメータ
[...catchAll].tsxキャッチオールルート
+not-found.tsxマッチしないURLのフォールバック
+html.tsxWeb向けHTMLカスタマイズ

Stackナビゲーションの実装

Stackナビゲーションは画面を積み重ねる形式で遷移する、モバイルアプリの基本パターンです。

ルートレイアウトでのStack設定

// app/_layout.tsx
import { Stack } from "expo-router";

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="index" options={{ title: "ホーム" }} />
      <Stack.Screen name="detail" options={{ title: "詳細" }} />
    </Stack>
  );
}

ヘッダーのカスタマイズ

Stack.Screenoptionsプロパティでヘッダーの見た目を細かく制御できます。

<Stack.Screen
  name="detail"
  options={{
    title: "詳細画面",
    headerStyle: { backgroundColor: "#6200ee" },
    headerTintColor: "#fff",
    headerTitleStyle: { fontWeight: "bold" },
  }}
/>

画面遷移の実行

LinkコンポーネントまたはuseRouterフックで遷移します。

import { Link, useRouter } from "expo-router";
import { View, Text, Pressable } from "react-native";

export default function HomeScreen() {
  const router = useRouter();

  return (
    <View>
      {/* 宣言的な遷移 */}
      <Link href="/detail">
        <Text>詳細へ</Text>
      </Link>

      {/* 命令的な遷移 */}
      <Pressable onPress={() => router.push("/detail")}>
        <Text>詳細へ移動</Text>
      </Pressable>
    </View>
  );
}

Tabsナビゲーションの実装

Tabsナビゲーションは画面下部にタブバーを表示し、タブの切り替えで画面遷移を行うパターンです。

タブレイアウトの定義

// app/(tabs)/_layout.tsx
import { Tabs } from "expo-router";
import { Ionicons } from "@expo/vector-icons";

export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: "#6200ee",
        headerShown: false,
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: "ホーム",
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="search"
        options={{
          title: "検索",
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="search" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: "プロフィール",
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

タブの表示・非表示を切り替える

特定の画面でタブバーを隠したい場合、tabBarStyledisplay: "none"を設定します。

<Tabs.Screen
  name="modal"
  options={{
    tabBarStyle: { display: "none" },
    tabBarButton: () => null, // タブバーからボタンも非表示
  }}
/>

Stack + Tabsの複合パターン

実際のアプリでは、タブ内にStack遷移を組み合わせるケースが一般的です。

ディレクトリ構成

app/
├── _layout.tsx           # ルート Stack
├── (tabs)/
│   ├── _layout.tsx       # Tabs定義
│   ├── home/
│   │   ├── _layout.tsx   # Home内のStack
│   │   ├── index.tsx     # /home
│   │   └── [id].tsx      # /home/:id
│   └── settings/
│       ├── _layout.tsx   # Settings内のStack
│       ├── index.tsx     # /settings
│       └── edit.tsx      # /settings/edit
└── modal.tsx             # モーダル画面

ルートレイアウト

// app/_layout.tsx
import { Stack } from "expo-router";

export default function RootLayout() {
  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Screen name="(tabs)" />
      <Stack.Screen
        name="modal"
        options={{ presentation: "modal" }}
      />
    </Stack>
  );
}

タブ内Stack

// app/(tabs)/home/_layout.tsx
import { Stack } from "expo-router";

export default function HomeLayout() {
  return (
    <Stack>
      <Stack.Screen name="index" options={{ title: "ホーム" }} />
      <Stack.Screen name="[id]" options={{ title: "アイテム詳細" }} />
    </Stack>
  );
}

このパターンにより、タブを維持したまま画面を深く掘り下げる遷移が実現できます。

主要Hooksの使い方

Expo Routerは画面遷移やパラメータ取得のためのHooksを提供しています。

useRouter

プログラムから画面遷移を実行するためのフックです。

import { useRouter } from "expo-router";

export default function MyScreen() {
  const router = useRouter();

  const navigateToDetail = (itemId: string) => {
    router.push(`/items/${itemId}`);
  };

  const goBack = () => {
    router.back();
  };

  const replaceScreen = () => {
    router.replace("/login");
  };

  // ...
}
メソッド動作
push(href)新しい画面をスタックに積む
replace(href)現在の画面を入れ替える(戻るボタンで前の画面に戻らない)
back()前の画面に戻る
canGoBack()戻れる画面があるか判定
dismiss()モーダルを閉じる
dismissAll()すべてのモーダルを閉じる

useLocalSearchParams

動的ルートのパラメータを取得します。現在の画面のパラメータのみを返すため、ネストされたナビゲーションでも正確に動作します。

// app/users/[id].tsx
import { useLocalSearchParams } from "expo-router";
import { View, Text } from "react-native";

export default function UserDetail() {
  const { id } = useLocalSearchParams<{ id: string }>();

  return (
    <View>
      <Text>ユーザーID: {id}</Text>
    </View>
  );
}

useGlobalSearchParams

useLocalSearchParamsがローカルなパラメータを返すのに対し、useGlobalSearchParamsはURLの全パラメータを返します。複数の画面がスタックに存在する場合に、最新のURLパラメータを参照する用途で使用します。

import { useGlobalSearchParams } from "expo-router";

export default function Header() {
  const params = useGlobalSearchParams();
  // URL全体のパラメータにアクセス
}

usePathname と useSegments

import { usePathname, useSegments } from "expo-router";

export default function DebugInfo() {
  const pathname = usePathname();   // 例: "/users/123"
  const segments = useSegments();   // 例: ["users", "123"]

  // 認証状態に応じたリダイレクトに活用
}

useFocusEffect

画面がフォーカスされたタイミングで処理を実行するフックです。React Navigationの同名フックと同じ動作をします。

import { useFocusEffect } from "expo-router";
import { useCallback } from "react";

export default function FeedScreen() {
  useFocusEffect(
    useCallback(() => {
      // 画面表示時にデータ取得
      fetchLatestPosts();

      return () => {
        // 画面離脱時のクリーンアップ
      };
    }, [])
  );
}

動的ルーティング

単一パラメータ

ファイル名をブラケットで囲むと、その部分が動的パラメータとなります。

app/
└── posts/
    └── [slug].tsx    # /posts/hello-world → slug = "hello-world"

キャッチオールルート

[...param]形式で、任意の深さのパスにマッチするルートを定義できます。

app/
└── docs/
    └── [...path].tsx  # /docs/a/b/c → path = ["a", "b", "c"]
// app/docs/[...path].tsx
import { useLocalSearchParams } from "expo-router";

export default function DocPage() {
  const { path } = useLocalSearchParams<{ path: string[] }>();
  // path は配列として取得される
}

React Navigationとの違い

Expo RouterはReact Navigationの上に構築されていますが、設計アプローチが大きく異なります。

観点Expo RouterReact Navigation
ルート定義ファイル配置で自動生成コード内で明示的に定義
型安全Typed Routesで自動生成手動で型定義が必要
Deep Linkingルートから自動設定別途linking設定が必要
Web対応標準搭載(ユニバーサル)react-native-webと組合せ
学習コストWebフレームワーク経験者は低いReact Navigation固有の概念
柔軟性ファイル構成に制約ありコードベースで自由に構成
Expo依存Expoプロジェクト必須Bare React Nativeでも利用可

選択の判断基準

Expo Routerが適するケース:

  • Expoで開発している
  • iOS・Android・Webの3プラットフォーム対応が必要
  • Next.jsやNuxtなどのファイルベースルーティング経験がある
  • Deep Linkingを手軽に設定したい
  • TypeScript型安全を自動で得たい

React Navigationが適するケース:

  • Bare React Native(Expoなし)で開発している
  • 高度にカスタマイズされたナビゲーション構造が必要
  • 既存のReact Navigationプロジェクトを維持する必要がある

Expoプロジェクトであれば、公式もExpo Routerの利用を推奨しています。React Navigationのドキュメントもスタイリングやオプション設定の参考として併用できます。

TypeScriptによる型安全な画面遷移

Expo RouterはTyped Routes機能により、存在するルートのみを型レベルで保証します。

Typed Routesの有効化

app.jsonに設定を追加します。

{
  "expo": {
    "experiments": {
      "typedRoutes": true
    }
  }
}

この設定により、expo start時にルート定義から型が自動生成されます。

型安全な遷移例

import { Link, useRouter } from "expo-router";

// 存在しないルートを指定するとTypeScriptエラーになる
<Link href="/users/123">ユーザー詳細</Link>  // OK
<Link href="/nonexistent">存在しない</Link>    // 型エラー

const router = useRouter();
router.push("/settings");    // OK
router.push("/unknown");     // 型エラー

パスパラメータの型も推論されるため、誤ったパラメータ名を使用した場合にビルド時点でエラーを検知できます。

Deep Linkingの自動設定

Expo Routerの大きな利点は、ルート定義から自動的にDeep Linkingが設定される点です。

カスタムURLスキーム

app.jsonschemeフィールドでカスタムスキームを定義します。

{
  "expo": {
    "scheme": "myapp"
  }
}

これによりmyapp://users/123のようなURLでアプリ内の画面に直接遷移できます。

Universal Links(iOS)/ App Links(Android)

HTTPSベースのDeep Linkingを設定する場合は、Apple App Site Association(AASA)ファイルやDigital Asset Linksの設定が必要です。Expoの設定ファイルでドメインを指定します。

{
  "expo": {
    "ios": {
      "associatedDomains": ["applinks:example.com"]
    },
    "android": {
      "intentFilters": [
        {
          "action": "VIEW",
          "autoVerify": true,
          "data": [
            { "scheme": "https", "host": "example.com", "pathPrefix": "/" }
          ],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    }
  }
}

ルートの追加・変更に連動してDeep Linkingのマッピングも更新されるため、手動でリンク定義を管理する負担がなくなります。

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

_layout.tsxの配置忘れ

ディレクトリを作成したのに画面が表示されない場合、_layout.tsxの配置漏れが原因であることが多いです。各ディレクトリには必ず_layout.tsxを配置し、ナビゲーション構造を定義してください。

// 最小限の_layout.tsx
import { Slot } from "expo-router";

export default function Layout() {
  return <Slot />;
}

Slotコンポーネントは子ルートをそのまま描画するだけのシンプルなレイアウトです。StackやTabsを使わない場合に利用します。

Expo Router利用時にNavigationContainerを自前で配置すると競合エラーが発生します。Expo Routerが内部でNavigationContainerを管理するため、アプリコード内では使用しないでください。

useLocalSearchParamsの型が合わない

動的ルートのパラメータはすべてstring型で取得されます。数値として使う場合は明示的な変換が必要です。

const { id } = useLocalSearchParams<{ id: string }>();
const numericId = Number(id); // 明示的に変換

タブ内Stack遷移でタブバーが消える

タブ内にStackを組み合わせる場合、タブ内のディレクトリに個別の_layout.tsxでStackを定義します。ルートレベルのStackにタブ外の画面を直接配置すると、タブバーが非表示になります。

バージョンとExpo SDKの対応

Expo SDKExpo RouterReact Native主な追加機能
SDK 53v50.79Protected Routes・ビルド時リダイレクト/リライト
SDK 54v60.81ネイティブタブ(アルファ)・Webモーダル・リンクプレビュー

バージョン間の互換性はExpo SDKに紐づいています。npx expo install expo-routerを実行すると、プロジェクトのSDKバージョンに合った互換バージョンが自動的にインストールされます。

まとめ

Expo Routerは、ファイル配置だけでルーティングを定義し、Stack・Tabs・動的ルートを組み合わせた実用的なナビゲーションを構築できるライブラリです。React Navigationを内部基盤としつつも、ファイルベースの設計パターンとTyped RoutesによるTypeScript型安全を実現しており、特にExpoで開発するiOS・Android・Web対応アプリに適しています。

Deep Linkingの自動設定やルートグループによるディレクトリ整理など、コードで手作業管理する部分を減らす設計思想は、チーム開発や中〜大規模プロジェクトでの保守性向上にも直結します。

公式ドキュメント: Expo Router - Expo Documentation