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 |
| TypeScript | Typed Routesによる型安全な画面遷移 |
| Deep Linking | ルート定義に基づいて自動設定 |
| SSG / SSR | Webプラットフォームで静的エクスポート対応 |
セットアップ手順
新規プロジェクトの場合
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.tsx | Web向け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.Screenのoptionsプロパティでヘッダーの見た目を細かく制御できます。
<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>
);
}
タブの表示・非表示を切り替える
特定の画面でタブバーを隠したい場合、tabBarStyleでdisplay: "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 Router | React 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.jsonのschemeフィールドでカスタムスキームを定義します。
{
"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を使わない場合に利用します。
NavigationContainer競合エラー
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 SDK | Expo Router | React Native | 主な追加機能 |
|---|---|---|---|
| SDK 53 | v5 | 0.79 | Protected Routes・ビルド時リダイレクト/リライト |
| SDK 54 | v6 | 0.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
