はじめに
React Nativeでアプリを作っていくと、必ずぶつかるのが画面遷移の設計です。ログイン画面から一覧画面へ、一覧から詳細へ、詳細からモーダルへ。さらに下部タブと横からのドロワーメニューが絡み合ってくると、「今どの画面がスタックのどこに積まれているのか」を自分で管理するのは現実的ではなくなってきます。
React Navigationは、このスタック・タブ・ドロワーといった画面遷移のパターンをNavigatorという単位でまとめ、宣言的に組み立てられるようにするルーティングライブラリです。iOS・Androidそれぞれのプラットフォームに合ったアニメーションやジェスチャーを標準で用意してくれるため、遷移のたびに独自実装をする必要がなくなります。
なお本記事では、他の記事で行っているようなLiveCodesの実行可能サンプルは掲載していません。理由は後述しますが、React Navigationはreact-native-screensやreact-native-gesture-handlerといったネイティブモジュールに依存しており、Web対応もReact Native Web経由でのバンドラ設定が前提となるため、ブラウザの単一ページ上で完結する形にできないためです。その代わり、実際のプロジェクトにそのまま貼り付けられるコード例を中心に解説します。
React Navigationとは
React Navigationは、React NativeおよびWebアプリ向けのルーティング・ナビゲーションライブラリです。GitHub上ではReact Nativeエコシステムの中でも最も採用されているナビゲーションライブラリの一つで、Expoの公式ドキュメントでも標準の選択肢として案内されています。
現在の主要パッケージは@react-navigation/nativeを中心に、@react-navigation/native-stack(スタック遷移)、@react-navigation/bottom-tabs(タブ遷移)、@react-navigation/drawer(ドロワーメニュー)といった役割ごとのパッケージに分かれています。かつて存在した統合パッケージreact-navigation(v1〜v4系)はすでに非推奨となっており、公式も@react-navigation/nativeへの移行を案内しています。
主な特徴
- 豊富なNavigatorの種類 - Stack・Tab・Drawer・Material Top Tabsなど、モバイルアプリでよく使う遷移パターンがひととおり揃っています
- プラットフォームネイティブな挙動 -
native-stackはiOS・Androidのネイティブナビゲーション実装をラップしており、スワイプバックなどのジェスチャーもOS標準の動きになります - TypeScriptとの親和性 - 画面ごとのパラメータの型を
ParamListとして定義でき、navigation.navigate()の呼び出しを型チェック付きで書けます - Static Configuration API - v7から導入された記法で、Navigatorの構成をオブジェクトとして宣言でき、深いネストになりがちな設定を見通しよく書けます
- ディープリンク対応 - URLパスとスクリーンを紐付ける設定を用意すれば、Web版はもちろんユニバーサルリンク・App Linksとの連携もそのまま行えます
インストール
Expoプロジェクトの場合は、以下のコマンドで導入します。
npx expo install @react-navigation/native @react-navigation/native-stack
React Native CLIの場合は、依存するネイティブモジュールも合わせてインストールします。
npm install @react-navigation/native @react-navigation/native-stack
npm install react-native-screens react-native-safe-area-context
iOSではPod install、Androidでは追加のGradle設定が必要になる場合があります。バージョン差異による不具合を避けるため、公式ドキュメントの「Getting started」に沿ってセットアップすることをおすすめします。
このようにReact Navigationは、react-native-screens(ネイティブ側の画面管理)やreact-native-gesture-handler(スワイプジェスチャー)といったネイティブモジュールと密に連携しています。Web対応も謳われていますが、実際に動かすにはWebpackやMetroでreact-nativeをreact-native-webにエイリアスする設定が必要で、この設定はプロジェクトのビルド構成に依存します。そのため、ブラウザ単体で完結するインタラクティブなサンプル(LiveCodesのようなプレイグラウンド)を本記事に埋め込むことができません。動かないサンプルを無理に置くよりは、実際のプロジェクトにそのまま持っていけるコードを示す方が有用だと考え、本記事ではその方針を取っています。
基本的な使い方
もっとも基本的な構成は、アプリ全体をNavigationContainerで包み、その中にStack.NavigatorとStack.Screenを並べる形です。createNativeStackNavigatorでNavigatorを作り、画面ごとにnameとcomponentを渡します。
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import HomeScreen from './HomeScreen';
import DetailScreen from './DetailScreen';
const Stack = createNativeStackNavigator();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Detail" component={DetailScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
画面遷移はuseNavigationフックで取得したnavigationオブジェクトのnavigate()を呼ぶだけです。
import { useNavigation } from '@react-navigation/native';
export default function HomeScreen() {
const navigation = useNavigation();
return (
<Button
title="詳細を見る"
onPress={() => navigation.navigate('Detail', { itemId: 42 })}
/>
);
}
navigate()の第二引数に渡したオブジェクトは、遷移先の画面でuseRoute().paramsから取り出せます。この「画面名+パラメータ」でルーティングを表現するスタイルが、React Navigationの基本的な考え方です。
実践的なユースケース
下部タブとスタックを組み合わせる
多くのアプリでは、下部タブの各タブがそれぞれ独立したスタック遷移を持ちます。createBottomTabNavigatorで作ったTabNavigatorの各Tab.Screenに、Stack.Navigatorをそのままコンポーネントとして渡すことで、この「タブの中にスタックがある」構成を素直に表現できます。
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Tab = createBottomTabNavigator();
const HomeStack = createNativeStackNavigator();
function HomeStackNavigator() {
return (
<HomeStack.Navigator>
<HomeStack.Screen name="Home" component={HomeScreen} />
<HomeStack.Screen name="Detail" component={DetailScreen} />
</HomeStack.Navigator>
);
}
export default function AppTabs() {
return (
<Tab.Navigator>
<Tab.Screen name="HomeTab" component={HomeStackNavigator} options={{ title: 'ホーム' }} />
<Tab.Screen name="Settings" component={SettingsScreen} />
</Tab.Navigator>
);
}
HomeTabから詳細画面に遷移してもタブバーは表示されたままになり、Instagramやメルカリのような「タブは固定、中身だけスタックで積む」というおなじみのUXがそのまま実現できます。
画面パラメータをTypeScriptで型安全にする
画面数が増えてくると、navigate('Detail', { itemId: 42 })のようなパラメータの受け渡しを、文字列や数値の取り違えなく管理したくなります。React NavigationではParamListという型を各Navigatorごとに定義し、NativeStackScreenPropsと組み合わせることで、navigate()の呼び出し自体を型チェック対象にできます。
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
type RootStackParamList = {
Home: undefined;
Detail: { itemId: number };
};
type Props = NativeStackScreenProps<RootStackParamList, 'Detail'>;
function DetailScreen({ route, navigation }: Props) {
const { itemId } = route.params; // itemIdはnumber型として推論される
return <Text>Item ID: {itemId}</Text>;
}
こうしておくと、navigation.navigate('Detail')のようにパラメータを渡し忘れた場合や、存在しない画面名を渡した場合に、実行前のコンパイル時点でエラーとして検出できます。
認証状態で画面グループを出し分ける
ログイン前後で表示するNavigator自体を丸ごと切り替えたいケースはよくあります。React Navigationでは特別なAPIを使わず、認証状態を持つstateやContextの値によって、レンダリングするStack.Screenの集合を条件分岐で切り替えるだけで実現できます。
function RootNavigator() {
const { isSignedIn } = useAuth();
return (
<Stack.Navigator>
{isSignedIn ? (
<Stack.Screen name="Home" component={HomeScreen} />
) : (
<Stack.Screen name="SignIn" component={SignInScreen} />
)}
</Stack.Navigator>
);
}
isSignedInが切り替わった瞬間に表示されるStack.Screenの中身が変わり、React Navigationが自動的に適切な画面遷移アニメーションを付けて切り替えてくれます。サインイン画面から戻るボタンでホーム画面に戻れてしまうといった事故も、この構成なら起こりません。
まとめ
React Navigationは、Stack・Tab・DrawerといったNavigatorを組み合わせるだけで、プラットフォームに沿った画面遷移を素早く実装できるライブラリです。TypeScriptによるパラメータの型付けや、Static Configuration APIによる宣言的な構成の書き方も整っており、画面数が増えていくプロジェクトほど恩恵を感じやすくなっています。
まずは@react-navigation/nativeとnative-stackを導入し、Home・Detailの2画面だけの小さなStack.Navigatorから触ってみることをおすすめします。慣れてきたらタブとの組み合わせ、TypeScriptでの型付け、認証状態による画面の出し分けへと段階的に進めていくと、無理なくReact Navigationの設計に馴染めるはずです。
