第9章 ナビゲーション ― Expo Router
Next.jsのApp Routerに近いファイルベースの発想で、画面と遷移を組み立てられるようになります。
モバイルアプリの大部分は「画面の集まり」と「画面間の遷移」でできています。本章では、画面遷移を担うExpo Routerの使い方を見ていきます。Next.jsのApp Routerに近いファイルベースの仕組みなので、Web開発の経験がそのまま活かせます。
なぜExpo Routerなのか
React Nativeのナビゲーションには、長らくReact Navigationというライブラリが使われてきました。Expo Routerはそれを内部で活用しつつ、ファイルベースのルーティングとして包み直したものです。
ファイルベースの何が嬉しいかというと、
- 画面を作る = ファイルを置く、で完結する(設定ファイルに配列を書き足す必要がない)
- ディレクトリ構成がそのままアプリの導線図になる
- ディープリンクが自動で対応する
- 型生成と組み合わせれば、画面遷移時のパス文字列も型安全になる
WebでNext.jsのpages/やapp/を書いてきた感覚を、ほぼそのまま持ち込めます。本書では新規プロジェクトはExpo Router前提で進めます。Web版も含めた使い勝手はExpo Routerで始めるNative & Webアプリケーションの快適なRoutingにまとめてあります。
ルートの基本
npx create-expo-appで作ったプロジェクトには、最初からapp/ディレクトリが用意されています。この中に置いたファイルがそのまま画面になります。
app/
├── _layout.tsx # ルートレイアウト
├── index.tsx # / に対応する画面(トップ)
├── about.tsx # /aboutに対応する画面
└── settings/
├── index.tsx # /settings
└── account.tsx # /settings/account
ファイル名 = URLパス、という対応関係はNext.jsと同じです。
画面コンポーネントの中身
各ファイルは、画面を表す関数コンポーネントをデフォルトエクスポートします。
// app/index.tsx
import { View, Text } from "react-native";
export default function HomeScreen() {
return (
<View style={{ flex: 1, padding: 16 }}>
<Text>トップ画面</Text>
</View>
);
}
普通のReactコンポーネントです。Expo Routerが「これがどのパスに対応する画面か」を、ファイルの位置から自動で判断してくれます。
レイアウトファイル _layout.tsx
_layout.tsxは、そのディレクトリ配下の画面に共通する「外枠」を定義するファイルです。Next.jsのlayout.tsxと同じ発想です。
// app/_layout.tsx
import { Stack } from "expo-router";
export default function RootLayout() {
return <Stack />;
}
ここでは「ルート直下のすべての画面はStack(積み重なる遷移)で扱う」と宣言しています。
レイアウトに使えるナビゲーター(画面の重ね方を決める仕組み)はいくつかあります。
ナビゲーターの種類
主要なものは4つです。
Stack
新しい画面を「上に積む」方式の遷移です。iOSの右からスライドイン、Androidのフェードのような、最も標準的なナビゲーションです。
import { Stack } from "expo-router";
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: "トップ" }} />
<Stack.Screen name="about" options={{ title: "Aboutページ" }} />
</Stack>
);
}
戻るボタン(左上の<)もExpo Router側が自動で表示してくれます。
Tabs
画面下部のタブで切り替える方式です。SNSやニュースアプリでよく見る構成です。
// app/(tabs)/_layout.tsx
import { Tabs } from "expo-router";
export default function TabsLayout() {
return (
<Tabs>
<Tabs.Screen name="index" options={{ title: "ホーム" }} />
<Tabs.Screen name="search" options={{ title: "検索" }} />
<Tabs.Screen name="profile" options={{ title: "プロフィール" }} />
</Tabs>
);
}
ディレクトリ名を(tabs)のように括弧で囲むと、URLには影響を与えずにグルーピングできます(これもNext.jsのRoute Groupと同じ考え方です)。
Native Tabs (SDK 55以降)
Expo SDK 55から、よりネイティブの見た目に忠実なNativeTabsが使えるようになりました。
iOSのTab Barはシステムから提供されるネイティブな部品なので、OSのバージョンが上がって見た目が変わったとき(Liquid Glass対応など)に追従しやすいのが利点です。第2章で触れたとおり、ライブラリ側の対応とビルドし直しは必要になりますが、自前で描いたタブバーのように一から作り直す必要はありません。プラットフォームの作法に最大限揃えたい場合は、TabsよりNativeTabsを選ぶのが現代の指針です。
SDK 55時点での書き方は次のようなイメージです(API名はSDKによって変わる可能性があります。最終的にはExpo公式ドキュメントで確認してください)。
// app/(tabs)/_layout.tsx
import { NativeTabs } from "expo-router/unstable-native-tabs";
export default function TabsLayout() {
return (
<NativeTabs>
<NativeTabs.Trigger name="index">
<NativeTabs.Trigger.Label>ホーム</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Icon sf="house.fill" drawable="ic_home" />
</NativeTabs.Trigger>
<NativeTabs.Trigger name="search">
<NativeTabs.Trigger.Label>検索</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Icon sf="magnifyingglass" drawable="ic_search" />
</NativeTabs.Trigger>
<NativeTabs.Trigger name="profile">
<NativeTabs.Trigger.Label>プロフィール</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Icon sf="person.fill" drawable="ic_person" />
</NativeTabs.Trigger>
</NativeTabs>
);
}
sfプロパティはiOS用にSF Symbolsの名前を、drawableプロパティはAndroid用にdrawableリソース名を指定します。アイコンもネイティブの仕組みで描画されるので、OSの見た目変更に自然と追従します。
「最初はTabsで実装して、デザインの方向性が決まってからNativeTabsに置き換える」という進め方も現実的です。
Modal
下からせり上がる、もしくは画面に重なって表示されるモーダル遷移です。Stackのpresentationオプションで指定します。
<Stack>
<Stack.Screen name="index" />
<Stack.Screen name="add-todo" options={{ presentation: "modal" }} />
</Stack>
設定画面、メニュー、画像詳細など「タスクから一時的に外れる」操作に向きます。

画面遷移のコード
画面間を遷移するには、<Link>コンポーネントか、routerオブジェクトを使います。
Linkで宣言的に遷移
import { Link } from "expo-router";
import { Text } from "react-native";
<Link href="/about">
<Text>Aboutへ</Text>
</Link>
Webの<a>に相当します。タップで指定パスへ遷移します。
routerで命令的に遷移
ボタンのonPress内など、イベントハンドラから遷移したい時はrouterを使います。
import { router } from "expo-router";
import { Pressable, Text } from "react-native";
<Pressable onPress={() => router.push("/about")}>
<Text>Aboutへ</Text>
</Pressable>
router.push、router.replace、router.back、router.dismissなど、用途に応じたメソッドが揃っています。
動的ルート
「TODOの詳細画面」のように、IDによって表示内容が変わる画面は、ファイル名を[id].tsxのように角括弧で囲みます。
app/
└── todos/
├── index.tsx # /todos
└── [id].tsx # /todos/123
画面側ではuseLocalSearchParamsで値を受け取ります。
// app/todos/[id].tsx
import { useLocalSearchParams } from "expo-router";
import { Text } from "react-native";
export default function TodoDetail() {
const { id } = useLocalSearchParams<{ id: string }>();
return <Text>TODO ID: {id}</Text>;
}
ジェネリクスで型を指定しておくと、idがstringとして受け取れます。
ディープリンク
モバイルアプリでは、myapp://todos/123のようなカスタムURLスキームや、https://example.com/todos/123のようなWebリンクからアプリの特定画面を直接開く「ディープリンク」が重要です。
Expo Routerはファイルベースのパスをそのままディープリンクのパスとして使えるため、追加の設定をほぼ書かずに対応できます。app.json(またはapp.config.ts)でスキームを宣言するだけです。
{
"expo": {
"scheme": "myapp"
}
}
これでmyapp://todos/123がapp/todos/[id].tsxに直接つながります。WebからのUniversal Links / App Linksを使う場合は、サーバ側のJSONファイル設定なども必要になりますが、基本の発想は同じです。
型安全なルーティング
Expo Routerには、ルート(画面パス)を型として生成する仕組みが用意されています。app.jsonまたはapp.config.tsでexperiments.typedRoutesを有効にすると、router.push("/todos/[id]")などのパス文字列が型チェックされます。
{
"expo": {
"experiments": { "typedRoutes": true }
}
}
タイプミスを防げるので、画面が増えてくるプロジェクトでは早めに有効化しておくのがおすすめです。
画面の組み合わせ例
実プロジェクトではよく、
- ルートはStackかTabs
- TabsのなかにそれぞれStackをネスト
- 詳細画面やメニューはModal
という組み合わせを取ります。Expo Routerはディレクトリ構成だけでこの構造を表現できます。
app/
├── _layout.tsx # 全体はStack
├── (tabs)/
│ ├── _layout.tsx # Tabsレイアウト
│ ├── index.tsx # ホームタブ
│ ├── search.tsx # 検索タブ
│ └── profile/
│ ├── _layout.tsx # プロフィール内のStack
│ ├── index.tsx
│ └── edit.tsx
└── settings.tsx # ルートStackの上に出るモーダルなど
ディレクトリを見ればアプリの構造が一目で分かる、というのがファイルベースルーティングの良さです。

ファイルとレイアウトの入れ子が、そのまま画面とナビゲーターの階層になります。
本章のまとめ
- 画面はappディレクトリにファイルを置けば作れる(ファイル名 = パス)
- 共通レイアウトは
_layout.tsxで定義 - ナビゲーターはStack / Tabs / NativeTabs / Modalの組み合わせ
- 遷移は
<Link>またはrouter.push - 動的ルートは
[id].tsx、useLocalSearchParamsで値を取得 - ディープリンクと型安全ルーティングも追加設定で対応できる
次章では、画面の中で扱うデータの話 ― 状態管理、データフェッチ、永続化に進みます。