React Nativeモバイルアプリ開発メモ 第2回。
前回はReact Native + Expoの開発環境を作るところまで整理した。今回はExpo Routerで画面遷移とアプリ構成を作る部分をまとめておく。
麻雀集計スマホアプリでは、最初はapp/フォルダ自体がなく、どのファイルが起点になるのか、App.tsxやindex.tsは使われるのか、_layout.tsxには何を書くのか、といったところから確認した。最終的にはExpo Routerを前提に、画面ファイルの配置とURLを対応させる構成にした。
Expo Routerはファイル配置がそのままルートになる
Expo Routerはファイルベースルーティング。基本的には、appまたはsrc/app配下のファイル構成がそのまま画面構成になる。
src/
└── app/
├── _layout.tsx
├── index.tsx
├── groups/
│ ├── index.tsx
│ └── [groupId].tsx
└── settings.tsx
この場合のイメージは次のとおり。
/ → index.tsx
/groups → groups/index.tsx
/groups/123 → groups/[groupId].tsx
/settings → settings.tsx
現在のExpo Router公式では、SDK 55以降の標準テンプレートはsrc/appを使う構成になっている。一方、以前作成した麻雀集計アプリはルート直下のapp/を使っていた。どちらも使えるが、両方を同時に置いた場合はsrc/appが優先されるので、途中で混在させない。
package.jsonのmainを確認する
既存プロジェクトへExpo Routerを追加するときに最初に確認するのがpackage.json。
{
"main": "expo-router/entry"
}
麻雀集計アプリでは、Expo Router導入前はapp/がなく、package.jsonにもExpo Router前提の設定がなかった。導入後はmainをexpo-router/entryにして、起動入口をExpo Routerへ切り替えた。
この構成なら、基本的な起動順は次のように考えると分かりやすかった。
npx expo start
↓
Metro
↓
expo-router/entry
↓
app/_layout.tsx または src/app/_layout.tsx
↓
Stack / Tabs
↓
index.tsxなどの各画面
そのため、Expo Router構成なのにApp.tsxや古いindex.tsを編集しても画面が変わらない、ということがある。
最小構成は _layout.tsx と index.tsx
最初は余計な画面を作らず、最低限この2ファイルで起動確認した方が分かりやすい。
src/
└── app/
├── _layout.tsx
└── index.tsx
_layout.tsxはナビゲーション全体の親になる。
import { Stack } from 'expo-router';
export default function RootLayout() {
return <Stack />;
}
index.tsxはトップ画面。
import { Text, View } from 'react-native';
export default function HomeScreen() {
return (
<View>
<Text>Home</Text>
</View>
);
}
Expo Routerでは各routeファイルにdefault exportが必要。画面ファイルを増やすときは、まずこれを確認する。
Root Layoutに共通Providerを置く
麻雀集計アプリでは、_layout.tsxにStackだけでなく、React Query、Portal、Gesture Handler、Safe Areaなどの共通処理も置いた。
概念的には次のような構成。
import { Stack } from 'expo-router';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
const queryClient = new QueryClient();
export default function RootLayout() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<QueryClientProvider client={queryClient}>
<Stack />
</QueryClientProvider>
</GestureHandlerRootView>
);
}
実際のプロジェクトではここにPortalHostやStatusBarなども入れた。アプリ全体で1回だけ必要なProviderはRoot Layoutへ置くと整理しやすい。
ただし、_layout.tsxでエラーになると全routeが読み込めなくなるので、一気にProviderを追加せず、まず<Stack />だけで動かしてから1つずつ追加した方が原因を追いやすい。
画面遷移はrouter.push
ボタンなどから別画面へ移動するときはrouter.push()をよく使う。
import { router } from 'expo-router';
import { Button } from 'react-native';
export default function HomeScreen() {
return (
<Button
title="グループ一覧"
onPress={() => router.push('/groups')}
/>
);
}
通常の画面遷移はpushでよい。戻る履歴も残る。
push・replace・backの使い分け
| 操作 | 用途 |
|---|---|
router.push() |
通常の画面遷移。履歴を積む |
router.replace() |
現在画面を置き換える。前画面へ戻らせたくない場合 |
router.back() |
1つ前の画面へ戻る |
router.navigate() |
既存のrouteへ移動する用途 |
麻雀集計アプリでは、ログイン後や初期遷移など「この画面には戻らせたくない」という場面ではreplace、通常の一覧→詳細ではpushという形で整理した。
動的ルートを使う
グループIDや大会IDなど、値によって表示内容が変わる画面は動的ルートにする。
src/
└── app/
└── groups/
└── [groupId].tsx
/groups/123ならgroupId = 123として受け取れる。
import { useLocalSearchParams } from 'expo-router';
export default function GroupScreen() {
const { groupId } =
useLocalSearchParams<{ groupId: string }>();
return null;
}
麻雀集計アプリでは、グループ、大会、卓などをこの方式で分けた。
groups/[groupId].tsx
tournaments/[tournamentId].tsx
tables/[tableId].tsx
IDをpropsで何階層も渡すより、route parameterから取得する方が画面単位で独立させやすかった。
route groupでURLを変えずに整理する
括弧付きのディレクトリはroute group。フォルダを整理できるが、URLには含まれない。
src/
└── app/
├── _layout.tsx
└── (tabs)/
├── _layout.tsx
├── index.tsx
└── settings.tsx
(tabs)はURLには出ない。画面構造だけをまとめたいときに便利。
StackとTabsを組み合わせる場合、詳細画面をどのStackに置くかで「戻る」の挙動が変わる。Expo公式でも、タブ内の詳細画面を開いた場合と、タブの外側のRoot Stackに詳細画面を置いた場合で戻り先が変わる。
戻る操作はWebより注意が必要だった
麻雀集計アプリをReact Native化したとき、画面表示そのものより「戻る」の方が複雑だった。
- Androidのシステム戻るボタン
- ヘッダーの戻るボタン
- 画面内の戻るボタン
- Bottom Tabs
- モーダル
- Deep Linkから直接開いた画面
通常のpushで開いた画面ならrouter.back()でよいが、Deep Linkから直接詳細画面を開いた場合は、期待する履歴が存在しないことがある。
Deep Linkそのものの設定は別記事に分けているので、ここでは「外部から直接画面を開いた場合、戻り先も設計する必要がある」とだけ覚えておく。
未保存データがある画面では戻る処理を考える
点数入力や編集画面では、単純にrouter.back()すると編集中の内容が消える可能性がある。
そのため、未保存データがある場合は確認モーダルを出すなど、ナビゲーションとは別に編集状態を管理する必要がある。
戻る操作
↓
変更あり?
├─ なし → router.back()
└─ あり → 確認モーダル
├─ 破棄 → back
└─ 継続 → そのまま
Webブラウザの戻るボタンだけを考えていたときより、スマホは戻る経路が多いので最初から設計しておく方がよかった。
実際に遭遇したトラブル
1. appフォルダがなく、どこから始めるのか分からなかった
Expo Router導入前のプロジェクトではapp/がなかった。まずExpo Router関連パッケージを導入し、package.jsonのmainを確認して、app/_layout.tsxとapp/index.tsxを作るところから始めた。
現在の新規テンプレートではsrc/appが標準なので、新規アプリではそちらを使う。既存プロジェクトでは今ある構成に合わせる。
2. App.tsxを編集しても表示が変わらない
main: "expo-router/entry"になっているExpo Routerプロジェクトでは、通常の起点は_layout.tsxと各routeファイル。古いApp.tsxやindex.tsが残っていても、そこが表示画面の起点とは限らない。
「コードを変えたのに画面が変わらない」ときは、まず実際のentry pointを確認する。
3. 多数のrouteで default export missing が出た
一度、_layout.tsx、index.tsx、グループ作成画面、卓画面、大会画面など、多数のrouteに対して一斉にmissing the required default exportが出た。
もちろん各routeにexport defaultが必要だが、複数ファイルが同時に全部エラーになった場合、各ファイルを1つずつ書き直す前にRoot Layoutや依存関係も疑った方がよい。
このときは_layout.tsx側でもStack付近でエラーが起きており、Expo RouterとReact Navigation関連の依存関係やimportを確認する必要があった。
4. ErrorBoundary is undefined
Expo Router内部のuseScreens.js付近で、次のようなエラーも出た。
Cannot read property 'ErrorBoundary' of undefined
このような内部エラーが出ると、routeファイルの問題に見えても、実際にはExpo Router、React Navigation、React Nativeのバージョン組み合わせや、Root Layoutで読み込んでいる依存の不整合が原因になっている場合がある。
麻雀集計アプリではExpo SDKに合う依存へ揃え、最終的にexpo-doctorが全項目通る状態まで調整した。
npx expo-doctor
npx expo install --check
npx expo install --fix
5. React Navigationを直接importしていた
Expo Routerを使う方針なのに、テーマ用ファイルなどに@react-navigation/nativeの直接importが残っていて依存関係が複雑になったこともあった。
Expo Routerが内部でReact Navigationを利用していても、アプリ側で直接使う必要がない部分までimportしない。未使用なら削除し、必要ならExpo SDKに合うバージョンをnpx expo installで入れる。
6. routeを直したのに表示が変わらない
route設定を変えたあとに古い状態が残っているように見える場合、Redirectやrouter.replace()で別画面へ飛ばしていないか確認する。
それでもおかしい場合はMetroキャッシュをクリア。
npx expo start --clear
麻雀集計アプリなら今はこう分ける
今作り直すなら、画面の役割ごとに次のように分ける。
src/
└── app/
├── _layout.tsx
├── index.tsx
├── groups/
│ ├── index.tsx
│ ├── create.tsx
│ └── [groupId].tsx
├── tournaments/
│ ├── [tournamentId].tsx
│ └── create.tsx
├── tables/
│ └── [tableId].tsx
└── settings.tsx
URL構造と画面構造が近くなるので、Deep Linkや共有URLを追加するときにも理解しやすい。
最初に確認するポイント
package.jsonのmainが何を指しているかsrc/appとappを混在させていないか- Root Layoutがまず
<Stack />だけで動くか - 各routeに
default exportがあるか router.pushとreplaceを使い分けているか- 動的ルートは
useLocalSearchParamsで取得できるか - 戻る履歴が存在しないDeep Link起点を考慮しているか
- 依存関係がおかしい場合は
expo-doctorを確認する
関連記事
開発環境そのものは、React Native + Expoでモバイルアプリ開発環境を作る【2026年版】にまとめている。
既存のReact/Vite版からスマホアプリへ移行したときの全体像は、React/ViteのWebアプリをReact Native化した記録に分けている。
外部URLから特定画面を開く処理は、ExpoでDeep Link / Android App Linksを設定した方法にまとめている。
公式ドキュメント
- Navigation layouts in Expo Router
- Navigating between pages in Expo Router
- Expo Router notation
- Using URL parameters
- Expo Router manual installation
最終更新:2026年10月22日
