Expo RouterによるReact Nativeファイルベースナビゲーション完全ガイド

Expo Routerを使ったReact Nativeのファイルベースルーティングを解説。レイアウト、ダイナミックルート、型安全なナビゲーション、タブ、モーダル、ミドルウェアなどの最新パターンを網羅します。

Expo Routerファイルベースナビゲーションシステム React Nativeモバイルアプリケーション向け

Expo Routerは、React Nativeにファイルベースルーティングをもたらし、手動のナビゲーション設定をNext.jsに触発された規約駆動のアプローチに置き換えます。Expo SDK 57とExpo Routerの組み合わせにより、Android、iOS、Webのクロスプラットフォームナビゲーションの構築は、適切なディレクトリにファイルを作成するだけで完了します。

クイックセットアップ

新規Expoプロジェクトでは、Expo Routerがデフォルトで組み込まれています。npx create-expo-app@latest --template defaultを実行すると、ファイルベースルーティングが事前設定されたプロジェクトが生成されます。既存プロジェクトの場合は、expo-routerパッケージをインストールし、エントリポイントを更新するだけで導入できます。

Expo Routerにおけるファイルベースルーティングの仕組み

appディレクトリ内に配置されたすべてのファイルが、自動的にルートとして認識されます。ファイルパスがそのままURLパスに対応するため、集中型のルーティング設定ファイルは一切不要です。例えば、app/settings.tsxを作成すると/settingsというルートが生成され、app/profile/edit.tsxは/profile/editに対応します。

この規約ベースのアプローチは、従来のReact Navigationによる手動設定と比較して、以下の3つの大きな利点をもたらします。

  • 設定不要: ファイルを作成した時点でルートが有効になる
  • 自動ディープリンク: すべての画面にURLが付与され、共有やテストが容易になる
  • 型付きナビゲーション: TypeScriptがコンパイル時に存在するルートを認識する
app/index.tsxtypescript
import { View, Text, StyleSheet } from 'react-native'
import { Link } from 'expo-router'

export default function HomeScreen() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Welcome</Text>
      {/* Link maps directly to file path */}
      <Link href="/settings" style={styles.link}>
        Open Settings
      </Link>
      <Link href="/profile/edit" style={styles.link}>
        Edit Profile
      </Link>
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', padding: 24 },
  title: { fontSize: 28, fontWeight: 'bold', marginBottom: 16 },
  link: { fontSize: 16, color: '#61DAFB', marginTop: 12 },
})

Linkコンポーネントは、すべてのプラットフォームでナビゲーションを処理します。Webでは適切なhref属性を持つアンカータグが生成され、SEOに対応した構造が得られます。ネイティブプラットフォームでは、スタック型のナビゲーション遷移がトリガーされます。

プロジェクト構成とレイアウトファイル

Expo Routerでは、_layout.tsxファイルを使用してナビゲーションコンテナを定義します。各ディレクトリに独自のレイアウトファイルを配置でき、ネストされたナビゲーション階層の構築が可能です。ルートレイアウトはアプリケーション全体を包括し、ネストされたレイアウトは特定のセクションを制御します。

典型的なプロジェクト構成は以下のとおりです。

text
app/
  _layout.tsx          # Root layout (Stack or custom)
  index.tsx            # Home screen (/)
  (tabs)/              # Tab group (parentheses = route group)
    _layout.tsx        # Tab navigator
    home.tsx           # /home tab
    search.tsx         # /search tab
    profile.tsx        # /profile tab
  settings/
    _layout.tsx        # Settings stack layout
    index.tsx          # /settings
    notifications.tsx  # /settings/notifications
    privacy.tsx        # /settings/privacy

ルートグループ(括弧で囲まれたディレクトリ)は、URLに影響を与えずにファイルを論理的に整理するための仕組みです。上記の(tabs)ディレクトリはタブナビゲータを構成しますが、URLは/tabs/homeではなく/home、/search、/profileのままです。

app/_layout.tsxtypescript
import { Stack } from 'expo-router'

export default function RootLayout() {
  return (
    <Stack
      screenOptions={{
        headerStyle: { backgroundColor: '#1a1a2e' },
        headerTintColor: '#ffffff',
        headerTitleStyle: { fontWeight: '600' },
      }}
    >
      <Stack.Screen name="index" options={{ title: 'Home' }} />
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen name="settings" options={{ title: 'Settings' }} />
    </Stack>
  )
}

ルートレイアウトは、フォントの読み込み、プロバイダーの設定、グローバル設定の初期化を行う場所でもあります。

Expo Routerによるタブナビゲーションの構築

タブナビゲーションを実装するには、ルートグループ内に_layout.tsxファイルを配置します。Expo Routerは、プラットフォーム固有のタブ体験を提供するNativeTabsと、ほとんどのユースケースに対応する標準のTabsコンポーネントの両方を提供します。SDK 56以降、Native Tabsはタブ選択を防ぎながらタブを表示したままにするdisabledプロパティをサポートしています。

app/(tabs)/_layout.tsxtypescript
import { Tabs } from 'expo-router'
import { Ionicons } from '@expo/vector-icons'

export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: '#61DAFB',
        tabBarInactiveTintColor: '#888',
        tabBarStyle: {
          backgroundColor: '#1a1a2e',
          borderTopColor: '#2d2d44',
        },
      }}
    >
      <Tabs.Screen
        name="home"
        options={{
          title: 'Home',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="search"
        options={{
          title: 'Search',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="search" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: 'Profile',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  )
}

各タブ画面のファイルは、標準的なReactコンポーネントをエクスポートするだけです。タブバーのアイコン、ラベル、バッジなどの設定は、レイアウトファイルのoptionsプロップで制御します。

ダイナミックルートとルートパラメータ

動的セグメントは、ファイル名に角括弧を使用して定義します。[id].tsxという名前のファイルは単一のセグメントに対応し、[...slug].tsxはパス内の残りのすべてのセグメントをキャプチャします。

app/product/[id].tsxtypescript
import { View, Text, StyleSheet } from 'react-native'
import { useLocalSearchParams, Stack } from 'expo-router'

export default function ProductScreen() {
  // Extract the dynamic parameter from the URL
  const { id } = useLocalSearchParams<{ id: string }>()

  return (
    <View style={styles.container}>
      <Stack.Screen options={{ title: `Product ${id}` }} />
      <Text style={styles.heading}>Product Details</Text>
      <Text style={styles.id}>ID: {id}</Text>
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 24 },
  heading: { fontSize: 24, fontWeight: 'bold', marginBottom: 8 },
  id: { fontSize: 16, color: '#888' },
})

/product/42にアクセスすると、この画面が表示され、id変数に"42"が設定されます。useLocalSearchParamsフックは、ルートパラメータへの型付きアクセスを提供します。

キャッチオールルートでは、[...slug].tsxがパスセグメント全体をキャプチャします。

app/docs/[...slug].tsxtypescript
import { useLocalSearchParams } from 'expo-router'

export default function DocsScreen() {
  // /docs/getting-started/installation → slug = ['getting-started', 'installation']
  const { slug } = useLocalSearchParams<{ slug: string[] }>()

  return <DocViewer path={slug.join('/')} />
}

React Nativeの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

型安全なルーティング(Typed Routes)

Expo Routerは、Typed Routes機能を有効にすると、ルートの型定義を自動生成します。この検証機能により、リンク切れをランタイムではなくコンパイル時に検出できます。

app.jsonで以下のように設定します。

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

この設定を有効にすると、Linkコンポーネントのhrefプロップやrouter.push()の引数には、実際に存在するルートに対応する文字列のみが許容されます。

app/checkout.tsxtypescript
import { router } from 'expo-router'

function handleCheckout(cartId: string) {
  // TypeScript validates this route exists
  router.push(`/product/${cartId}`)

  // This would cause a compile error if /nonexistent doesn't exist
  // router.push('/nonexistent')
}

Typed RoutesはuseLocalSearchParamsと組み合わせることで特に効果を発揮します。自動生成された型定義により、ルート定義内のパラメータ名とコンシューマコンポーネントで使用されるパラメータ名の一致が保証されます。特定の画面に遷移した際にのみ発見されるような微妙なバグを未然に防止できます。

モーダル画面とプレゼンテーション設定

Expo Routerにおけるモーダルは、レイアウトでpresentation: 'modal'を指定した通常の画面です。ファイルベースルーティングの規約に忠実に、モーダルも単なるルートの1つとして扱われます。

app/_layout.tsxtypescript
import { Stack } from 'expo-router'

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="index" />
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      {/* Modal screen slides up from the bottom */}
      <Stack.Screen
        name="create-post"
        options={{
          presentation: 'modal',
          headerTitle: 'New Post',
        }}
      />
    </Stack>
  )
}
app/create-post.tsxtypescript
import { View, TextInput, Button, StyleSheet } from 'react-native'
import { router } from 'expo-router'
import { useState } from 'react'

export default function CreatePostModal() {
  const [title, setTitle] = useState('')

  const handleSubmit = () => {
    // Submit logic here
    router.back() // Dismiss the modal
  }

  return (
    <View style={styles.container}>
      <TextInput
        style={styles.input}
        placeholder="Post title"
        value={title}
        onChangeText={setTitle}
      />
      <Button title="Publish" onPress={handleSubmit} />
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 24 },
  input: {
    borderWidth: 1,
    borderColor: '#333',
    borderRadius: 8,
    padding: 12,
    fontSize: 16,
    marginBottom: 16,
  },
})

/create-postにナビゲートすると、モーダルプレゼンテーションが起動します。router.back()を呼び出すことでモーダルが閉じられ、ナビゲーションスタックの前の画面に戻ります。

プログラムによるナビゲーションとRouter API

Linkコンポーネント以外に、Expo Routerはrouterオブジェクトを通じた命令的なAPIを提供します。このAPIにより、ユーザーの直接的な操作ではなく、ビジネスロジックに基づいたナビゲーション制御が可能になります。

typescript
import { router } from 'expo-router'

// Push a new screen onto the stack
router.push('/profile/settings')

// Replace the current screen (no back button)
router.replace('/login')

// Go back to the previous screen
router.back()

// Navigate with parameters
router.push({
  pathname: '/product/[id]',
  params: { id: '42', source: 'recommendations' },
})

// Check if going back is possible
import { useRouter } from 'expo-router'

function BackButton() {
  const router = useRouter()
  return router.canGoBack() ? (
    <Button title="Back" onPress={() => router.back()} />
  ) : null
}

router.pushとrouter.replaceの使い分けは、React Nativeの認証フローにおいて特に重要です。ログイン成功後にrouter.replace('/dashboard')を使用すると、戻るボタンでログイン画面に戻れなくなり、適切なユーザー体験が実現されます。

ナビゲーションとリダイレクト

router.replace()はナビゲーション履歴の現在のエントリを置き換えます。未認証ユーザーをリダイレクトする認証ガードでは、コンポーネントのレンダリング内で<Redirect href="/login" />を使用することが推奨されます。Redirectコンポーネントはレンダリングフェーズで実行され、Webにおけるサーバーサイドレンダリングとも正しく連携します。

ミドルウェアとルート保護

Expo Routerは、ルートレベルのロジックを処理するサーバーミドルウェアを提供します。+middleware.tsファイルは、リクエストがルートコンポーネントに到達する前にインターセプトし、認証チェック、リダイレクト、HTTPヘッダーの操作を可能にします。

app/+middleware.tstypescript
import { type MiddlewareRequest } from 'expo-router/server'

export function middleware(request: MiddlewareRequest) {
  const { pathname } = request.nextUrl

  // Protect dashboard routes
  const protectedPaths = ['/dashboard', '/settings', '/profile']
  const isProtected = protectedPaths.some(p => pathname.startsWith(p))

  if (isProtected) {
    const token = request.cookies.get('session')
    if (!token) {
      return Response.redirect(new URL('/login', request.url))
    }
  }

  return undefined // Continue to route
}

ネイティブプラットフォームでは、サーバーミドルウェアは実行されません。そのため、ルート保護にはクライアントサイドのガードが必要です。最も一般的なパターンは、保護されたレイアウトを認証チェックで囲む方法です。

app/(authenticated)/_layout.tsxtypescript
import { Redirect, Stack } from 'expo-router'
import { useAuth } from '@/hooks/useAuth'

export default function AuthenticatedLayout() {
  const { isLoggedIn, isLoading } = useAuth()

  if (isLoading) return null
  if (!isLoggedIn) return <Redirect href="/login" />

  return <Stack />
}
プラットフォームの違い

サーバーミドルウェアは、サーバーサイドレンダリングを使用するWebでのみ実行されます。ネイティブアプリケーションでは、レイアウトコンポーネント内にクライアントサイドのガードを必ず実装する必要があります。両方のアプローチを組み合わせることで、すべてのプラットフォームにおいて一貫したルート保護が実現されます。

SDK 56からの移行: React Navigationフォーク

Expo SDK 56以降、Expo Routerは@react-navigation/*パッケージに依存しなくなりました。プロジェクトでReact Navigationから直接インポートしている場合は、インポートを更新する必要があります。

元のパッケージ新しい場所
@react-navigation/nativeexpo-router/react-navigation
@react-navigation/stackexpo-router/js-stack
@react-navigation/bottom-tabsexpo-router/js-tabs

コードモッドを実行してインポートを自動的に更新できます。

bash
npx expo-codemod sdk-56-expo-router-react-navigation-replace src

ランタイムAPIは変更されていません。モジュール指定子のみが移動しました。@react-navigation/coreから引き続きインポートしているサードパーティライブラリは、Expo CLIがバンドル時にこれらのインポートを書き換えるため、自動的に動作します。

ソース

Expo Routerに関する面接での期待事項

  • Expo Routerはappディレクトリ内の各ファイルを自動的にルートにマッピングし、手動のナビゲーション設定を不要にする
  • _layout.tsxファイルで定義されたレイアウトは、スタック、タブ、ドロワーのナビゲーション階層を作成する
  • [param].tsxによるダイナミックルートと[...slug].tsxによるキャッチオールルートは、完全なTypeScriptサポートでパラメータ付きナビゲーションを処理する
  • app.jsonで有効にした型付きルートは、コンパイル時に壊れたナビゲーションリンクを検出する
  • モーダル画面、router.push/replace/backによるプログラム的ナビゲーション、サーバーミドルウェアがルーティングツールキットを完成させる
  • 括弧で囲まれたルートグループは、URLに影響を与えずにコードを整理する
  • レイアウト内のクライアントサイドガードがネイティブでの認証を処理し、サーバーミドルウェアがWebをカバーする
  • SDK 56以降、@react-navigation/*からのインポートはexpo-router/*の同等物に更新する必要がある

React Nativeのアーキテクチャと面接準備については、ネイティブモジュールのモジュールを参照してください。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

今日のチャレンジ

React Native のバグを見つけられますか

実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年10月6日 更新

タグ

#react-native
#expo
#expo-router
#navigation
#mobile-development
#tutorial

共有

関連記事