本文書ではReact Nativeにナビゲーション(ルーティング, ページ移動, Stack)を実装する際に利用されるExpo Routerの基本的な設定方法について動作確認を行いながら説明を行なっています。

本ブログの記事の公開時ののExpo Routerのバージョン3.5で現在も活発に開発が行われています。

動作確認を行なったmacOSのバージョンはsonoma14.5, Xcodeのバージョンは15.3です。

Expo Routerとは

React Nativeでナビゲーションを実装する場合にExpo Routerの他にはReact NavigationライブラリをがありますがReact Nativeの開発ツールであるExpoを利用する場合には特に理由がない限り新しいプロジェクトではExpo Routerを利用します。Expo RouterとReact Navigationは全く別のライブラリではなくExpo RouterはReact Navigationを使って作成されており、React Navigationの機能を活用してReact Navigationよりも簡単にナビゲーションの設定を行うことができます。

Expo RouterとReact Navigatigationの大きな違いはルーティングの設定です。Expo Routerではファイルシステムルーティングを採用しているのでReactのメタフレームワークNext.jsと同様にディレクトリの中にファイルを保存するとディレクトリとファイルの構造により自動でルーティングが設定されます。

手動でルーティングを設定するReact Navigationについては本サイトでも公開済みので参考にしてください。

プロジェクトの作成

Expo RouterはExpoで作成したプロジェクトで利用することができるのでプロジェクトの作成は”npx create-expo-app@latest”コマンドで行います。–templateオプションを指定することでインストール時にテンプレートを選択することができます。 –templateオプションにblankを指定しない場合はコマンド実行後にテンプレートを選択することができます。TypeScriptを利用するかどうかもテンプレートで選択することができますが本文書ではJavaScriptの”Blank”を指定しています。


% npx create-expo-app@latest --template blank
✔ What is your app named? … react-navive-expo-router
✔ Downloaded and extracted project files.
> npm install
//略
✅ Your project is ready!

To run your project, navigate to the directory and run one of the following npm commands.

- cd react-navive-expo-router
- npm run android
- npm run ios
- npm run web

プロジェクトの作成後はプロジェクトディレクトリに移動します。


% cd react-navive-expo-router

Expo Routerのインストール

プロジェクトの作成時に”Blank”templateを指定した場合はExpo Routerのインストールは行われていないのでExpo Routerに関係するライブラリのインストールを行う必要があります。


 % npx expo install expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar
Blank以外を選択した場合はExpo Routerは自動でインストールされます。
fukidashi

Expo Routerのインストールが完了したら、package.jsonファイルを開いてエントリーポイントの設定を行うことができるmainの値をデフォルトの”expo/AppEntry.js”から”expo-router/entry”に変更します。


{
  "name": "react-navive-expo-router",
  "version": "1.0.0",
  "main": "expo-router/entry",
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web"
  },
  "dependencies": {
    "expo": "~51.0.18",
    "expo-status-bar": "~1.12.1",
    "react": "18.2.0",
    "react-native": "0.74.3",
    "expo-router": "~3.5.17",
    "react-native-safe-area-context": "4.10.1",
    "react-native-screens": "3.31.1",
    "expo-linking": "~6.3.1",
    "expo-constants": "~16.0.2"
  },
  "devDependencies": {
    "@babel/core": "^7.20.0"
  },
  "private": true
}

次にapp.jsonファイルにschamaプロパティを追加して任意のアプリケーションの名前を設定しておきます。ここでは”react-navive-expo-router”としています。


{
  "expo": {
    "name": "react-navive-expo-router",
    "slug": "react-navive-expo-router",
    "scheme": "react-navive-expo-router",
    "version": "1.0.0",
//略

schemeを設定しない場合は”npm start”コマンドを実行した時にWARNINGが表示されます。設定しない場合は開発環境では動作しても本番環境では正常に動作しなくなる可能性があるようです。


 WARN  Linking requires a build-time setting `scheme` in the project's Expo config (app.config.js or app.json) for production apps, if it's left blank, your app may crash. The scheme does not apply to development in the Expo client but you should add it as soon as you start working with Linking to avoid creating a broken build. Learn more: https://docs.expo.dev/guides/linking/
警告: リンキングには、本番アプリのプロジェクトのExpo設定(app.config.jsまたはapp.json)にビルド時の設定schemeが必要です。これが空白のままだと、アプリがクラッシュする可能性があります。schemeはExpoクライアントでの開発には適用されませんが、リンキングを使用し始めるとすぐに壊れたビルドを作成しないように追加するべきです。詳細はこちらを参照してください: https://docs.expo.dev/guides/linking/

はじめてのExpo Router

インストールが完了したらデフォルトで作成されているプロジェクトディレクトリのApp.jsファイルを削除してappディレクトリを作成し、その下にindex.jsファイルを作成します。index.jsファイルを作成することで自動でルーティングが設定されます。


import { Text } from 'react-native';

export default function Page() {
  return <Text>Home page</Text>;
}

“npm start”コマンドで開発サーバを起動します。Expoで構築したアプリケーションの動作確認はXcodeのSimulatorか”Expo Go”を利用して行うことができます。手元のiPhoneに”Expo Go”をインストールした場合は”npm start”コマンド実行後に表示されるQRコードをカメラにかざすとiPhone上でアプリケーションが起動します。

appディレクトリの下にさらにabout.jsファイルを作成して以下のコードを記述します。


import { Text } from 'react-native';

export default function Page() {
  return <Text>About page</Text>;
}

index..js, about.jsを作成するとファイルベースルーティング機能により自動で/, /aboutのルーティングが作成されます。index.jsの内容はアプリケーションの初期画面となりますが、/aboutのルーティングが正常に作成されているか確認するためにindex.jsのページから/aboutのAboutページ(about.js)に移動できるようにLinkコンポーネントを利用します。Linkコンポーネントを利用することでページ間を移動することができます。Linkコンポーネントのhref propsにはAboutページのパスである/aboutを設定します。


import { Link } from 'expo-router';
import { Text, View } from 'react-native';

export default function Page() {
  return (
    <View>
      <Text>Home page</Text>
      <Link href="/about">About</Link>
    </View>
  );
}

画面上には”Home page”とaboutページへのリンクが設定された文字列Aboutが表示されます。

Homeページの表示とリンク
Homeページの表示とリンク

Aboutの文字列をクリックするとAboutページに移動することができます。

Aboutページの表示
Aboutページの表示

appディレクトリにファイルを作成するだけで自動でルーティングが設定され、Linkコンポーネントを利用してページの移動が行えることが確認できました。

Layout Routesの設定

index.js, about.jsファイルを使って2つのページを作成しましたが通常はアプリケーションでは各ページはコンテンツ以外にヘッダーやフッターなどで構成されそれらの要素は複数のページで共有します。Expo Routerでは共有できる要素をLayout Routesで設定することができます。

appディレクトリの下に_layout.jsファイルを作成します。ファイル名の先頭は”_”(アンダーバー)です。


import { Text } from 'react-native';

export default function Layout() {
  return <Text>レイアウト</Text>;
}

設定後アプリケーションを確認するとレイアウトの文字のみ左上に表示されそれまで表示されていた”Home page”の文字列が表示されなくなります。

Layout Routesの表示
Layout Routesの表示

index.jsやabout.jsファイルで設定した内容を表示させるためLayout RouteではSlotコンポーネントを利用します。


import { Slot } from 'expo-router';
import { Text, View } from 'react-native';

export default function HomeLayout() {
  return (
    <View>
      <Text>レイアウト</Text>
      <Slot />
    </View>
  );
}

画面上にはindex.jsの内容が表示されます。

Slotコンポーネントの設定
Slotコンポーネントの設定

表示を画面に収まるようにするためにSafeAreaViewコンポーネントを利用することができます。_layout.jsファイルでViewタグをSafeAreaViewタグに変更すると画面に収まるようになります。

SafeAreaViewを利用した場合
SafeAreaViewを利用した場合

Navigation Pattern

Layout RoutesではSlotコンポーネントを利用することでindex.js, about.jsに記述したコンテンツを表示することができ、Linkコンポーネントでページを移動することができました。

Slotコンポーネントを利用せずページ間を移動するためのナビゲーションがExpo Routerにはいくつか準備されていますがその中からモバイルアプリでの利用頻度の高いStackとTabsのNavigationについて動作確認を行います。Stack, Tabs以外のNavigationにDrawer、Modalなどがあります。

Stack Navigation

Expo Routerの基本のナビゲーションであるStack Navigationを利用しナビゲーションの設定を行います。名前にStackと入っている通り、Stackを利用してページの移動を管理します。Stackは階層構造を持ちページを移動するとStackの上に情報が追加され、元のページに戻るとStackから削除されます。Stackを利用することでページに移動したり、移動したページから戻ったりすることができます。

Stack Navigationの設定にはStackコンポーネントを利用します。appディレクトリの_layout.jsファイルで設定していたSlotをStackに置き換えます。


import { Stack } from 'expo-router';

export default function HomeLayout() {
  return <Stack />
  );
}

Stackに置き換えただけで画面の上部にヘッダーが表示されその中にファイルの名前に対応するindexの文字列が自動で表示されます。

Stack Navigationの設定
Stack Navigationの設定

表示されているページのaboutページへのリンクであるAboutの文字列をクリックしてください。

ヘッダーにはファイルに対応するaboutという文字列だけではなくindexページに戻るためのリンクも自動で表示されます。indexをクリックするとindexページに戻ります。このようにStack Navigationを利用することでページを行ったり、来たりすることが簡単にできるようになります。

ページの移動元の情報がヘッダーに表示
ページの移動元の情報がヘッダーに表示

画面上の上部に表示されているヘッダーの色や文字のスタイルをカスタマイズしたい場合にはpropsのscreenOptionsを利用して行うことができます。ヘッダーの背景色を”#f4511e”、文字色を白、文字の太さをBoldに設定しています。


import { Stack } from 'expo-router';

export default function Layout() {
  return (
    <Stack
      screenOptions={{
        headerStyle: {
          backgroundColor: '#f4511e',
        },
        headerTintColor: '#fff',
        headerTitleStyle: {
          fontWeight: 'bold',
        },
      }}
    />
  );
}

設定した通り、ヘッダーの背景や文字の色やスタイルが変更されることが確認できます。

ヘッダーのカスタマイズ
ヘッダーのカスタマイズ

ページ毎に表示されているindex, aboutの文字列(title)を変更したい場合にはStack.Screenのname propsにページの名前を指定してoption propsでtitleプロパティを設定することが変更できます。ヘッダーの色も各ページで個別設定することができます。


import { Stack } from 'expo-router';

export default function Layout() {
  return (
    <Stack
      screenOptions={{
        headerStyle: {
          backgroundColor: '#f4511e',
        },
        headerTintColor: '#fff',
        headerTitleStyle: {
          fontWeight: 'bold',
        },
      }}
    >
      <Stack.Screen name="index" options={{ title: 'ホーム' }} />
      <Stack.Screen
        name="about"
        options={{
          headerStyle: { backgroundColor: '#ddd' },
          title: '私たちについて',
        }}
      />
    </Stack>
  );
}
ヘッダーのタイトルを変更
ヘッダーのタイトルを変更
タイトルと背景色を更新
タイトルと背景色を更新

optionの設定は_layout.jsファイルだけではなくindex.js, about.jsファイルの中で設定することもできます。index.js, about.jsで設定を行うため_layout.jsファイルで既存Stack.Screenの設定を削除します。


import { Stack } from 'expo-router';

export default function Layout() {
  return (
    <Stack
      screenOptions={{
        headerStyle: {
          backgroundColor: '#f4511e',
        },
        headerTintColor: '#fff',
        headerTitleStyle: {
          fontWeight: 'bold',
        },
      }}
    />
  );
}

index.jsファイルにStack.Screenを追加してoptionsでtitleを設定します。設定後、_layout.jsで設定した内容と同じ内容で設定できることが確認できます。


import { Stack, Link } from 'expo-router';
import { Text, View } from 'react-native';

export default function Page() {
  return (
    <View>
      <Stack.Screen
        options={{
          title: 'ホーム',
        }}
      />
      <Text>Home page</Text>
      <Link href="/about">About</Link>
    </View>
  );
}

Tabs Navigation

Tabsはアプリケーションの下部(フッター部分)にアイコンを表示してアイコンをクリックすることでページ移動を行うことができるナビゲーションです。Stack Navigationと同様にモバイルアプリでは必ず利用されているナビゲーションの一つです。

設定にはTabsコンポーネントを利用します。appディレクトリの_layout.jsファイルで以下のように記述します。


import { Tabs } from 'expo-router';

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

index.jsとabout.jsファイルは以下のとおりです。タブでページ移動が行えるのでindex.jsファイルからaboutへ移動するためのLinkコンポーネントは削除しています。


import { Text } from 'react-native';

export default function Page() {
  return <Text>Home page</Text>;
}

import { Text } from 'react-native';

export default function Page() {
  return <Text>About page</Text>;
}

設定後は画面上のフッター部分にindex, aboutのボタンが表示され、クリックするとページに移動することができます。

Tab Navigationの表示
Tab Navigationの表示

画面の下部を拡大すると下記のように表示されます。

拡大画面
拡大画面

ヘッダーのtitleは_layout.jsファイルのTabs.Screenで設定することができます。


import { Tabs } from 'expo-router';

export default function Layout() {
  return (
    <Tabs>
      <Tabs.Screen name="index" options={{ title: 'ホーム' }} />
      <Tabs.Screen name="about" options={{ title: '私たちについて' }} />
    </Tabs>
  );
}

タイトルを更新すると下部のTabの文字も一緒に更新されます。

タイトルを更新
タイトルを更新
Tabの文字列の更新
Tabの文字列の更新

アイコンの設定

デフォルトでは画面で確認した通り逆三角の記号が表示されています。アイコンを設定したい場合にはoptionsのtabBarIconで設定することができます。アイコンは追加インストールなしに@expo/vector-iconsからimportして利用することができます。


import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';

export default function Layout() {
  return (
    <Tabs>
      <Tabs.Screen
        name="index"
        options={{
          title: 'ホーム',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen name="about" options={{ title: '私たちについて' }} />
    </Tabs>
  );
}

逆三角からホームのアイコンに変わっていることが確認できます。

タブに表示されているアイコンの設定
タブに表示されているアイコンの設定

Stack+Tabs Navigatoin

StackとTabs Navigationはアプリケーション内でどちらか一方のみ設定できるというわけではなく一緒に利用することができます。

StackとTabsを一緒に設定した場合の動作確認を行います。Stack Navigationで設定するページの一つにTabs Navigationを設定していきます。Tabs Navigationを追加するためにappの_layout.jsファイルではindex(index.jsに対応), about(about.jsに対応)の設定に加えて(tabs)を設定しています。()はGroup Syntaxと呼ばれグループ化することができるので(tabs)ディレクトリを作成してその下に_layout.js, index.js, profile.jsファイルを作成することで(tabs)ディレクトリのみに適用されるLayoutを設定することができます。


import { Stack } from 'expo-router';

export default function Layout() {
  return (
    <Stack>
      <Stack.Screen name="index" />
      <Stack.Screen name="about" />
      <Stack.Screen name="(tabs)" />
    </Stack>
  );
}

作成した(tabs)ディレクトリ下のファイルを利用してTab Navigationを設定することができます。(tabs)ディレクトリの_layout.jsファイルでTabsコンポーネントを設定します。


import { Tabs } from 'expo-router';

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

(tabs)ディレクトリの下のindex.js, profile.jsファイルには以下のコードを記述します。


import { Text } from 'react-native';

export default function Page() {
  return <Text>Tabs Index page</Text>;
}

import { Text } from 'react-native';

export default function Page() {
  return <Text>Profile page</Text>;
}

(tabs)ディレクトリに作成したindex.js, profile.jsは_layout.jsファイルでTabsコンポーネントを設定しているので画面のフッターにTabとして表示されます。

/app/index.jsから(tabs)のページにアクセスるためにはhrefに(tabs)を設定することができます。


import { Link } from 'expo-router';
import { Text, View } from 'react-native';

export default function Page() {
  return (
    <View>
      <Text>Home page</Text>
      <Link href="/about">About</Link>
      <Link href="(tabs)">Tabs</Link>
    </View>
  );
}

画面上にTabsとAboutへのリンクを持つ文字列が表示されます。

Stack+Tabs Navigationの初期ページ
Stack+Tabs Navigationの初期ページ

Tabsをクリックすると以下のようにヘッダーには(tabs)、indexの文字列が2段になって表示されます。フッターには(tabs)ディレクトリ内にあるindex.js, profileのタブのみ表示されます。app/_layout.jsで設定しているStack.Screenのaboutが表示されることはありません。

StackからTabs Navigationへ
StackからTabs Navigationへ

ヘッダーを拡大するとStack Navigationの一部でもあるのでヘッダーにはクリックを行ったindexページへのリンクが表示されています。

ヘッダーの確認
ヘッダーの確認

フッターにはindex, profileのタブのみ表示されています。

フッターの拡大
フッターの拡大
AboutのリンクをクリックするとAboutページに移動することができます。
fukidashi

表示されているヘッダーのタイトルの(tabs)を非表示にするために下記のようにapp/_layout.jsのStack.ScreenのoptionsでheaderShownオプションを利用します。


import { Stack } from 'expo-router';

export default function Layout() {
  return (
    <Stack>
      <Stack.Screen name="index" />
      <Stack.Screen name="about" />
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
    </Stack>
  );
}

ヘッダーから(tabs)の文字列とindexページに戻るためのリンクが非表示となります。

ヘッダーから(tabs)を非表示
ヘッダーから(tabs)を非表示

ヘッダーのタイトルのindexを変更したい場合にはapp/(tabs)/_layout.jsファイルで行うことができます。


import { Tabs } from 'expo-router';

export default function Layout() {
  return (
    <Tabs>
      <Tabs.Screen name="index" options={{ title: 'タブホーム' }} />
      <Tabs.Screen name="profile" />
    </Tabs>
  );
}

indexからoptionsのtitleプロパティで設定した”タブホーム”に変更されていることが確認できます。

ページのtitleの設定
ページのtitleの設定

フッターを見るとヘッダーのtitleだけではなくTabの文字列も一緒に更新されます。

フッターの文字列の確認
フッターの文字列の確認

Stack+Tabsの2つを利用したNavigation設定を確認することができました。

Tabs+Stack Navigatoin

先ほどはStack Navigationの下にTabs Navigationを設定しましたがここではその逆のTabs Navigationの下にStack Navigationの設定ができることを確認します。

(tabs)のディレクトリ名を(stacks)に変更してapp/_layout.jsファイルをStack NavigationからTabs Navigationの設定に変更します。


import { Tabs } from 'expo-router';

export default function Layout() {
  return (
    <Tabs>
      <Tabs.Screen name="index" />
      <Tabs.Screen name="about" />
      <Tabs.Screen name="(stacks)" />
    </Tabs>
  );
}

(stacks)ディレクトリのindex.jsとprofile.jsは以下のとおりです。


import { Text, View } from 'react-native';

export default function Page() {
  return <Text>Stack Index page</Text>;
}

import { Text } from 'react-native';

export default function Page() {
  return <Text>Profile page</Text>;
}

(stacks)ディレクトリの_layout.jsをTabsコンポーネントからStackコンポーネントに変更します。


import { Stack } from 'expo-router';

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

アプリケーションの画面を確認するとフッターには3つのTabが表示されます。1つのTabは(stacks)となっています。

フッターのTabを確認
フッターのTabを確認

(stacks)のタブをクリックすると下記の画面に表示されます。

(stacks)ページの確認
(stacks)ページの確認

ヘッダー部分を拡大して確認すると以下のように(stacks)とindexが表示されます。

ヘッダーの確認
ヘッダーの確認

Tabの(stacks)を”スタック”の文字列に変更するためにapp/_layout.jsファイルの(stacks)のTabs.Screenのoptionsでtitleを変更します。


import { Tabs } from 'expo-router';

export default function Layout() {
  return (
    <Tabs>
      <Tabs.Screen name="index" />
      <Tabs.Screen name="about" />
      <Tabs.Screen name="(stacks)" options={{ title: 'スタック' }} />
    </Tabs>
  );
}
(stacks)からスタックの変更
(stacks)からスタックの変更

ヘッダーだけではなくTabsの文字列も(stacks)から変更した”スタック”に更新されていることが確認できます。

(stacks)からスタックへの更新
(stacks)からスタックへの更新

ヘッダーのスタックを非表示にするためにheaderShownプロパティをfalseにします。


import { Tabs } from 'expo-router';

export default function Layout() {
  return (
    <Tabs>
      <Tabs.Screen name="index" />
      <Tabs.Screen name="about" />
      <Tabs.Screen
        name="(stacks)"
        options={{ title: 'スタック', headerShown: false }}
      />
    </Tabs>
  );
}

表示されていたヘッダーのスタックが非表示となります。

(stacks)を非表示に
(stacks)を非表示に

フッターに表示されているTabのスタックはそのままです。

(stacks)からスタックへの更新
(stacks)からスタックへの更新

ヘッダーのindexを変更したい場合にはapp/(stacks)/_layout.jsファイルで設定します。


import { Stack } from 'expo-router';

export default function Layout() {
  return (
    <Stack>
      <Stack.Screen name="index" options={{ title: 'スタックホーム' }} />
      <Stack.Screen name="profile" options={{ title: 'プロファイル' }} />
    </Stack>
  );
}

indexからoptionsのtitleプロパティで設定した”スタックホーム”に変更されます。

ページのtitleの設定
ページのtitleの設定

/app/(stacks)/index.jsの”スタックホーム”ページから”プロファイル”ページへの移動を確認します。


import { Link } from 'expo-router';
import { Text, View } from 'react-native';

export default function Page() {
  return (
    <View>
      <Text>Stack Index page</Text>
      <Link href="/(stacks)/profile">Profile</Link>
    </View>
  );
}
href=”/profile”でもページの移動することができます。appの直下に(stacks)下の同じ名前のファイルprofile.jsが存在しない場合にはhref=”/profile”ではapp直下に設定されたprofileのページに移動します。
fukidashi
Profileのリンクの設定
Profileのリンクの設定

Stack Navigationで設定されているのでindexページ(タブホーム)からprofileページに移動するとヘッダーには移動元の”スタックホーム”のリンクが表示されます。

Profileページへの移動
Profileページへの移動

Tabs+Stackの2つを利用したNavigation設定を確認することができました。

Hooksによるページの移動

useNavigation Hook

Linkコンポーネントを利用してページの移動を行っていましたがHookを利用してページ移動を行うことができます。

app/index.jsからapp/about.jsの移動をuseNavigation Hookを利用して行うことができます。


import { useNavigation } from 'expo-router';
import { Text, View } from 'react-native';

export default function Page() {
  const navigation = useNavigation();

  return (
    <View>
      <Text>Home page</Text>
      <Text
        onPress={() => {
          navigation.navigate('about');
        }}
      >
        About Page
      </Text>
    </View>
  );
}

useRouter Hook

useNavigation Hookと同様にuseRouter Hookを利用してページを移動することができます。


import { useRouter } from 'expo-router';
import { Text, View } from 'react-native';

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

  return (
    <View>
      <Text>Home page</Text>
      <Text
        onPress={() => {
          router.push('/about');
        }}
      >
        About Page
      </Text>
    </View>
  );
}

routerオプジェクトにはそのほかにnavigate, replace, back, canGoBack, setParamsメソッドがあります。

ここまでの動作確認でExpo Routerの基本的な動作を確認することができました。