Article

React Native Navigation 7実装パターン10選

高田晃太郎
React Native Navigation 7実装パターン10選

npmの公開統計では@react-navigation/nativeの週次ダウンロードが直近で100万件超に達し、React Nativeの事実上の標準ナビゲーションとして採用が進んでいます[2]。2024〜2025年にかけて安定化したReact Navigation 7は、型安全性とパフォーマンス、Web互換性のバランスが向上し[1]、Expo Router v3系の基盤としても選ばれています[3]。native-stackの最適化や画面のfreeze戦略により、初回タブ切替えの体感遅延や描画のばらつきの改善が期待でき、設計段階でルーティングの型境界を明確化しておくと、バグの早期検出やQAコストの圧縮につながる可能性があります。ここでは、現場で汎用性が高かった10の実装パターンを、設計、UX/パフォーマンス、運用・拡張の3ブロックで整理します。前提はReact Native 0.74以上、React Navigation 7系、Hermes有効のリリースビルドを想定します。参考として、代表的な依存関係は次のとおりです。

{
  "dependencies": {
    "@react-navigation/native": "^7.0.0",
    "@react-navigation/native-stack": "^7.0.0",
    "@react-navigation/bottom-tabs": "^7.0.0",
    "react-native-screens": "~3.30.0",
    "react-native-safe-area-context": "^4.10.0",
    "@react-native-async-storage/async-storage": "^1.23.0"
  }
}

設計と型安全:壊れにくいルーティングを先に作る

最初のブロックでは、後戻りコストを大きく左右する型と構成の決め方に集中します。React Navigation 7はTypeScriptの型補完が成熟しており、ネスト構成でもNavigatorScreenParamsを使った境界の明示で、安全に規模を伸ばせます[5]。ここでいうParamListは「ルート名とパラメータ型の辞書」、NavigatorScreenParamsは「ネスト先ナビゲータのParamListを受け渡すための型」です。

パターン1:TypeScriptのParamListでネスト境界を型安全に固める

型安全の土台はParamListの分割と合成です。ルート名をリテラルとして一元管理し、ネストする場合はNavigatorScreenParamsで渡し口を定義します。これにより、誤ったパラメータや名前のタイポをコンパイル時に検知できます[5]。

import { NavigatorScreenParams } from '@react-navigation/native';

export type MainTabsParamList = {
  Home: undefined;
  Search: { q?: string } | undefined;
  Settings: undefined;
};

export type RootStackParamList = {
  Auth: undefined;
  Main: NavigatorScreenParams<MainTabsParamList>;
  Modal: { id: string };
};

画面からの遷移も型で守られます。間違ったパラメータキーを渡した場合に即座にエラーとなり、ランタイムのクラッシュを未然に防ぎます[5]。

import { NativeStackScreenProps } from '@react-navigation/native-stack';
import type { RootStackParamList } from './types';

type Props = NativeStackScreenProps<RootStackParamList, 'Modal'>;

export function HomeScreen({ navigation }: Props) {
  return (
    <Button title="Open" onPress={() => navigation.navigate('Modal', { id: '123' })} />
  );
}

パターン2:認証ガードはナビゲータ分割で表現する

画面ごとにガードするのではなく、認証状態に応じて出し分けるスタックを切り替えるのが保守性に優れます。React Navigation 7ではコンテナレベルでの条件分岐が素直に書け、初期描画のちらつきも抑えられます[6]。

import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useAuth } from './auth';
import type { RootStackParamList } from './types';

const Stack = createNativeStackNavigator<RootStackParamList>();

export default function AppNavigator() {
  const { user, initializing } = useAuth();

  if (initializing) return null; // スプラッシュと組み合わせる

  return (
    <NavigationContainer>
      <Stack.Navigator screenOptions={{ headerShown: false }}>
        {user ? (
          <Stack.Screen name="Main" component={MainTabs} />
        ) : (
          <Stack.Screen name="Auth" component={AuthStack} />
        )}
        <Stack.Screen name="Modal" component={ModalScreen} options={{ presentation: 'modal' }} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

この分割はA/Bテストやロールアウトの切替にも強く、ビジネス要件の変化に追従しやすくなります[6]。

パターン3:Deep Linkは先に設計し、Linkingを型と同居させる

Deep Link(URLでアプリ内画面へ遷移する仕組み)は後付けすると壊れがちです。型で確定したルート構造からリンクマッピングを生成する方針にすると、認知負荷が下がります。7系ではgetStateFromPathの改善により、ネストされたタブ遷移の再現も安定しています[1]。

import { LinkingOptions } from '@react-navigation/native';
import type { RootStackParamList } from './types';

export const linking: LinkingOptions<RootStackParamList> = {
  prefixes: ['myapp://', 'https://example.com'],
  config: {
    screens: {
      Auth: 'auth',
      Main: {
        screens: {
          Home: '',
          Search: 'search/:q?',
          Settings: 'settings'
        }
      },
      Modal: 'item/:id'
    }
  }
};

UXとパフォーマンス:体感速度に効く工夫

次は体感性能と操作感に直結するポイントです。native-stack(iOS/Androidのネイティブ遷移を活用)とreact-native-screens(ネイティブレベルで画面を効率管理)の組み合わせを軸に、不要な再レンダリングを断ち、遷移の一貫性を高めます[4]。detachInactiveScreensやfreezeOnBlurの併用は、メモリフットプリントやCPUスパイクの抑制に寄与することが報告されています。測定環境の一例としては、iPhone 14 Pro、iOS 17、RN 0.74、Hermes有効、Releaseビルドなどが参考になります[4]。

パターン4:Native Stack + react-native-screensの標準設定を固める

NavigationContainerとNative Stackの初期化時に、screensの最適化とジェスチャーを明示します。これだけでベースラインの体感が変わります[4]。

import { NavigationContainer, DefaultTheme } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { enableScreens } from 'react-native-screens';

enableScreens(true);

const Stack = createNativeStackNavigator();

export function Root() {
  return (
    <NavigationContainer theme={DefaultTheme}>
      <Stack.Navigator
        screenOptions={{
          headerBackTitleVisible: false,
          gestureEnabled: true,
          animation: 'slide_from_right'
        }}
      >
        {/* screens */}
      </Stack.Navigator>
    </NavigationContainer>
  );
}

パターン5:Bottom Tabsはlazyとfreezeで「使うまで動かさない」

初期描画コストを削るには、タブ配下を必要になるまでレンダリングしない方針が効きます。freezeOnBlur(非アクティブ時にコンポーネントを凍結)により副作用を止め、再フォーカス時の再計算を避けられます[7]。

import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Platform } from 'react-native';

const Tab = createBottomTabNavigator();

export function MainTabs() {
  return (
    <Tab.Navigator
      screenOptions={{
        lazy: true,
        freezeOnBlur: true,
        tabBarHideOnKeyboard: Platform.OS === 'android',
        headerShown: false
      }}
    >
      <Tab.Screen name="Home" component={HomeScreen} />
      <Tab.Screen name="Search" component={SearchScreen} />
      <Tab.Screen name="Settings" component={SettingsScreen} />
    </Tab.Navigator>
  );
}

パターン6:モーダルとシートを一貫表現し、状態を保つ

一時的な詳細表示はモーダルスタックにまとめます。iOSのpresentationとアニメーションを統一し、下層画面の状態を保ったまま中断・再開できるようにします。Bottom Sheetを使う場合もルーティング経由に寄せるとテスト容易性が上がります[8]。

import { createNativeStackNavigator } from '@react-navigation/native-stack';

const Stack = createNativeStackNavigator();

export function ModalGroup() {
  return (
    <Stack.Group screenOptions={{ presentation: 'modal', animation: 'fade' }}>
      <Stack.Screen name="Modal" component={ModalScreen} />
    </Stack.Group>
  );
}

パターン7:beforeRemoveで未保存データを保護する

フォーム画面では戻る操作のフックが安全策になります。7系でもイベントハンドラは変わらず、UIスレッドをブロックしない実装が推奨です[9]。

import { useEffect } from 'react';
import { Alert } from 'react-native';
import { useNavigation, useRoute } from '@react-navigation/native';

export function EditProfileScreen() {
  const navigation = useNavigation();
  const route = useRoute();
  const hasUnsaved = true; // フラグは実装に合わせて

  useEffect(() => {
    const sub = navigation.addListener('beforeRemove', (e) => {
      if (!hasUnsaved) return;
      e.preventDefault();
      Alert.alert('変更を破棄しますか?', '保存していない変更があります。', [
        { text: 'キャンセル', style: 'cancel' },
        {
          text: '破棄',
          style: 'destructive',
          onPress: () => navigation.dispatch(e.data.action)
        }
      ]);
    });
    return sub;
  }, [navigation, route, hasUnsaved]);

  return null;
}

パターン8:ヘビースクリーンを分割し、フォーカス時にデータ更新する

一覧画面など負荷の高いコンポーネントは、ルーティングと描画領域を疎結合にし、フォーカス時にクエリを走らせます。useFocusEffectを使うと、タブ再フォーカス時の更新が書きやすくなります[10]。

import { useFocusEffect } from '@react-navigation/native';
import { useCallback } from 'react';

export function FeedScreen() {
  useFocusEffect(
    useCallback(() => {
      let alive = true;
      (async () => {
        try {
          // 取得
        } catch (e) {
          // ログ
        }
      })();
      return () => {
        alive = false;
      };
    }, [])
  );
  return null;
}

運用・拡張:Deep Link、状態保全、計測まで

最後のブロックはプロダクション運用を安定させるための設計です。リンク、状態保全、観測の三点を固めると、障害時の切り分けと再現が格段に楽になります。ここではLinkingの堅牢化と永続化、そして計測とテーマ貫通を10パターンに集約します。

パターン9:Linkingの堅牢化とユニバーサルリンク、状態永続化の併用

Linkingはエラー前提で扱います。パス解釈失敗時のフォールバックと、認証状態に応じた遷移制御を実装しておくと安全です[1,6]。Webやデスクトップ対応を視野に入れるなら、originのホワイトリストも検討に値します[1]。

import { NavigationContainerRef, getStateFromPath } from '@react-navigation/native';
import * as Linking from 'expo-linking';
import { linking } from './linking';

export const navRef = { current: null as unknown as NavigationContainerRef<any> };

export function App() {
  return (
    <NavigationContainer
      ref={(r) => (navRef.current = r!)}
      linking={{
        ...linking,
        getStateFromPath: (path, options) => {
          try {
            return getStateFromPath(path, options);
          } catch (e) {
            // ログ送出し、ホームにフォールバック
            return undefined;
          }
        }
      }}
      onReady={() => {
        const url = Linking.useURL?.() ?? null;
        // 初期URL検査など
      }}
    >{/* ... */}</NavigationContainer>
  );
}

突然のクラッシュやOSによるプロセス回収に備え、ナビゲーション状態をAsyncStorageに保存しておくと、ユーザーは直前の画面から再開できます。復元時のスキーマ差分には注意し、破損データを握りつぶすガードを必ず入れます[11]。

import AsyncStorage from '@react-native-async-storage/async-storage';
import { NavigationContainer, InitialState } from '@react-navigation/native';
import { useEffect, useState } from 'react';

const NAV_STATE_KEY = 'NAVIGATION_STATE_V1';

export function PersistedContainer({ children }: { children: React.ReactNode }) {
  const [initialState, setInitialState] = useState<InitialState | undefined>();
  const [ready, setReady] = useState(false);

  useEffect(() => {
    (async () => {
      try {
        const saved = await AsyncStorage.getItem(NAV_STATE_KEY);
        if (saved) {
          const parsed = JSON.parse(saved);
          setInitialState(parsed);
        }
      } catch (e) {
        // 破損データは無視
        await AsyncStorage.removeItem(NAV_STATE_KEY);
      } finally {
        setReady(true);
      }
    })();
  }, []);

  if (!ready) return null;

  return (
    <NavigationContainer
      initialState={initialState}
      onStateChange={async (state) => {
        try {
          await AsyncStorage.setItem(NAV_STATE_KEY, JSON.stringify(state));
        } catch {}
      }}
    >
      {children}
    </NavigationContainer>
  );
}

パターン10:画面計測を一箇所に集約し、テーマ/アクセシビリティをナビゲーションに貫通させる

onStateChangeとrefから現在のルートを抽出し、分析基盤へ送ります。画面名は型から導出される文字列リテラルに限定すると、ダッシュボードの集計が安定します。決済や会員登録などクリティカルフローは、ナビゲーションイベントとビジネスイベントの両方を送って突き合わせると因果が見えやすくなります[12]。

import analytics from '@segment/analytics-react-native';
import { NavigationContainer, NavigationContainerRef } from '@react-navigation/native';

const navigationRef = { current: null as unknown as NavigationContainerRef<any> };

function getActiveRouteName(state: any): string | undefined {
  const route = state?.routes?.[state.index ?? 0];
  if (!route) return undefined;
  if (route.state) return getActiveRouteName(route.state);
  return route.name;
}

export function TrackedContainer({ children }: { children: React.ReactNode }) {
  return (
    <NavigationContainer
      ref={(r) => (navigationRef.current = r!)}
      onStateChange={(state) => {
        const name = getActiveRouteName(state);
        if (name) analytics.screen(name);
      }}
    >
      {children}
    </NavigationContainer>
  );
}

OSのカラースキームやフォントスケールはNavigationContainerのテーマに反映させ、ヘッダやタブのコントラストを確保します。これによりWCAG観点の不備を早期に潰せます[13]。

import { NavigationContainer, Theme, DefaultTheme, DarkTheme } from '@react-navigation/native';
import { useColorScheme } from 'react-native';

const Light: Theme = { ...DefaultTheme, colors: { ...DefaultTheme.colors, background: '#fff' } };
const Dark: Theme = { ...DarkTheme, colors: { ...DarkTheme.colors, background: '#000' } };

export function ThemedContainer({ children }: { children: React.ReactNode }) {
  const scheme = useColorScheme();
  return <NavigationContainer theme={scheme === 'dark' ? Dark : Light}>{children}</NavigationContainer>;
}

環境とROIの視点:導入期間と効果の目安

React Navigation 6から7への移行は、中規模アプリで1〜2スプリントが目安になることが多いです。ParamListの整備とLinkingの先出し設計に約1週間、freezeやlazyの最適化と計測の実装に数日、残りをE2Eでの回帰と端末差分の検証に当てる進め方が現実的です。運用開始後は、戻る制御や状態保全により問い合わせ対応時間の縮小が見込まれ、ルート名の型保障によって不具合修正の探索時間も短縮しやすくなります。ビジネスKPIとの相関はプロダクト固有ですが、ナビゲーションを土台から整えることが継続的な開発速度とクラッシュ率低減に寄与しやすいのは、多くのチームで再現しやすい傾向があります[1]。

まとめ:壊れにくい道筋を最初に敷く

React Navigation 7は、型安全、パフォーマンス、運用性の三拍子がそろった現場指向のナビゲーション基盤です。ParamListでネスト境界を固定し、認証フローはナビゲータ単位で切り替え、Linkingは最初に設計して型の傘の下に入れるという順番を守るだけで、後工程の手戻りは大きく減らせます。UXの体感改善はfreezeやlazyのような小さなスイッチの積み重ねで達成しやすく、状態保全と計測を同居させると障害時の復元と原因特定が速くなります。自分たちのプロダクトの痛点はどこにあるのか、今日のブランチで型とLinkingから整備し直してみませんか。次の一歩として、既存のParamListにNavigatorScreenParamsを導入し、最も重いタブにfreezeOnBlurを適用して計測するところから始めるのが効果的です。

参考文献

  1. React Navigation Team. React Navigation 7.0 (2024-11-06). https://reactnavigation.org/blog/2024/11/06/react-navigation-7.0
  2. npmjs.com. @react-navigation/native Weekly Downloads. https://www.npmjs.com/package/@react-navigation/native
  3. Expo Docs. Migrate to Expo Router from React Navigation. https://docs.expo.dev/router/migrate/from-react-navigation/
  4. React Navigation Docs. react-native-screens and performance. https://reactnavigation.org/docs/5.x/react-native-screens
  5. React Navigation Docs. TypeScript. https://reactnavigation.org/docs/typescript
  6. React Navigation Docs. Authentication flows. https://reactnavigation.org/docs/auth-flow
  7. React Navigation Docs. Bottom Tab Navigator. https://reactnavigation.org/docs/bottom-tab-navigator
  8. React Navigation Docs. Modal in React Navigation 7.x. https://reactnavigation.org/docs/7.x/modal
  9. React Navigation Docs. usePreventRemove / beforeRemove. https://reactnavigation.org/docs/use-prevent-remove
  10. React Navigation Docs. useFocusEffect. https://reactnavigation.org/docs/use-focus-effect
  11. React Navigation Docs. State persistence. https://reactnavigation.org/docs/state-persistence
  12. React Navigation Docs. Screen tracking. https://reactnavigation.org/docs/screen-tracking
  13. React Navigation Docs. Themes. https://reactnavigation.org/docs/themes