コンテンツにスキップ
Webフロントエンドエンジニアのための React Native 実践入門
Esc
navigateopen⌘Jpreview
このページの内容

第9章 ナビゲーション ― Expo Router

Next.jsのApp Routerに近いファイルベースの発想で、画面と遷移を組み立てられるようになります。

モバイルアプリの大部分は「画面の集まり」と「画面間の遷移」でできています。本章では、画面遷移を担うExpo Routerの使い方を見ていきます。Next.jsのApp Routerに近いファイルベースの仕組みなので、Web開発の経験がそのまま活かせます。

なぜExpo Routerなのか

React Nativeのナビゲーションには、長らくReact Navigationというライブラリが使われてきました。Expo Routerはそれを内部で活用しつつ、ファイルベースのルーティングとして包み直したものです。

ファイルベースの何が嬉しいかというと、

  • 画面を作る = ファイルを置く、で完結する(設定ファイルに配列を書き足す必要がない)
  • ディレクトリ構成がそのままアプリの導線図になる
  • ディープリンクが自動で対応する
  • 型生成と組み合わせれば、画面遷移時のパス文字列も型安全になる

WebでNext.jspages/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に置き換える」という進め方も現実的です。

下からせり上がる、もしくは画面に重なって表示されるモーダル遷移です。Stackpresentationオプションで指定します。

<Stack>
  <Stack.Screen name="index" />
  <Stack.Screen name="add-todo" options={{ presentation: "modal" }} />
</Stack>

設定画面、メニュー、画像詳細など「タスクから一時的に外れる」操作に向きます。

push、back、replace、Modalそれぞれの履歴と画面表示の違い

画面遷移のコード

画面間を遷移するには、<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.pushrouter.replacerouter.backrouter.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>;
}

ジェネリクスで型を指定しておくと、idstringとして受け取れます。

ディープリンク

モバイルアプリでは、myapp://todos/123のようなカスタムURLスキームや、https://example.com/todos/123のようなWebリンクからアプリの特定画面を直接開く「ディープリンク」が重要です。

Expo Routerはファイルベースのパスをそのままディープリンクのパスとして使えるため、追加の設定をほぼ書かずに対応できます。app.json(またはapp.config.ts)でスキームを宣言するだけです。

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

これでmyapp://todos/123app/todos/[id].tsxに直接つながります。WebからのUniversal Links / App Linksを使う場合は、サーバ側のJSONファイル設定なども必要になりますが、基本の発想は同じです。

型安全なルーティング

Expo Routerには、ルート(画面パス)を型として生成する仕組みが用意されています。app.jsonまたはapp.config.tsexperiments.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の上に出るモーダルなど

ディレクトリを見ればアプリの構造が一目で分かる、というのがファイルベースルーティングの良さです。

ファイル構成がStack、Tabs、詳細画面、Modalへ対応する様子

ファイルとレイアウトの入れ子が、そのまま画面とナビゲーターの階層になります。

本章のまとめ

  • 画面はappディレクトリにファイルを置けば作れる(ファイル名 = パス)
  • 共通レイアウトは_layout.tsxで定義
  • ナビゲーターはStack / Tabs / NativeTabs / Modalの組み合わせ
  • 遷移は<Link>またはrouter.push
  • 動的ルートは[id].tsxuseLocalSearchParamsで値を取得
  • ディープリンクと型安全ルーティングも追加設定で対応できる

次章では、画面の中で扱うデータの話 ― 状態管理、データフェッチ、永続化に進みます。

最終更新 2026年7月24日