jpskill.com
🛠️ 開発・MCP コミュニティ

rn-navigation

Expo Router navigation patterns for React Native. Use when implementing navigation, routing, deep links, tab bars, modals, or handling navigation state in Expo apps.

⚡ おすすめ: コマンド1行でインストール(60秒)

下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。

🍎 Mac / 🐧 Linux
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o rn-navigation.zip https://jpskill.com/download/17858.zip && unzip -o rn-navigation.zip && rm rn-navigation.zip
🪟 Windows (PowerShell)
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/17858.zip -OutFile "$d\rn-navigation.zip"; Expand-Archive "$d\rn-navigation.zip" -DestinationPath $d -Force; ri "$d\rn-navigation.zip"

完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。

💾 手動でダウンロードしたい(コマンドが難しい人向け)
  1. 1. 下の青いボタンを押して rn-navigation.zip をダウンロード
  2. 2. ZIPファイルをダブルクリックで解凍 → rn-navigation フォルダができる
  3. 3. そのフォルダを C:\Users\あなたの名前\.claude\skills\(Win)または ~/.claude/skills/(Mac)へ移動
  4. 4. Claude Code を再起動

⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。

🎯 このSkillでできること

下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。

📦 インストール方法 (3ステップ)

  1. 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
  2. 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
  3. 3. 展開してできたフォルダを、ホームフォルダの .claude/skills/ に置く
    • · macOS / Linux: ~/.claude/skills/
    • · Windows: %USERPROFILE%\.claude\skills\

Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。

詳しい使い方ガイドを見る →
最終更新
2026-05-18
取得日時
2026-05-18
同梱ファイル
1

📖 Skill本文(日本語訳)

※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。

React Native Navigation (Expo Router)

ファイルベースルーティングの基礎

Expo Router はファイルシステムベースのルーティングを使用します。app/ 内のファイルがルートになります。

ルート構造

app/
├── _layout.tsx          # ルートレイアウト (プロバイダー、グローバル UI)
├── index.tsx            # "/" ルート
├── (tabs)/              # タブグループ (括弧 = レイアウトグループ)
│   ├── _layout.tsx      # タブバーの設定
│   ├── home.tsx         # "/home" タブ
│   └── profile.tsx      # "/profile" タブ
├── (auth)/              # 認証フローグループ
│   ├── _layout.tsx      # 認証固有のレイアウト
│   ├── login.tsx        # "/login"
│   └── register.tsx     # "/register"
├── settings/
│   ├── index.tsx        # "/settings"
│   └── [id].tsx         # "/settings/123" (動的)
└── [...missing].tsx     # キャッチオール 404

レイアウトグループ (groupName)

括弧はレイアウトグループを作成します。これらはレイアウトの階層構造には影響しますが、URL には影響しません。

// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';

export default function TabLayout() {
  return (
    <Tabs>
      <Tabs.Screen 
        name="home" 
        options={{ 
          title: 'Home',
          tabBarIcon: ({ color }) => <HomeIcon color={color} />,
        }} 
      />
      <Tabs.Screen 
        name="profile" 
        options={{ title: 'Profile' }} 
      />
    </Tabs>
  );
}

動的ルート [param]

// app/player/[id].tsx
import { useLocalSearchParams } from 'expo-router';

export default function PlayerScreen() {
  const { id } = useLocalSearchParams<{ id: string }>();

  return <PlayerProfile playerId={id} />;
}

キャッチオールルート [...slug]

// app/[...missing].tsx
import { Link, Stack } from 'expo-router';

export default function NotFound() {
  return (
    <>
      <Stack.Screen options={{ title: 'Not Found' }} />
      <Link href="/">Go home</Link>
    </>
  );
}

ナビゲーションパターン

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

import { useRouter, Link } from 'expo-router';

function MyComponent() {
  const router = useRouter();

  // push でナビゲート (スタックに追加)
  router.push('/player/123');

  // パラメータ付きでナビゲート
  router.push({
    pathname: '/player/[id]',
    params: { id: '123' },
  });

  // 現在の画面を置き換え (戻ることはできません)
  router.replace('/home');

  // 戻る
  router.back();

  // ルートにナビゲート
  router.navigate('/');

  // モーダルを閉じる
  router.dismiss();
}

Link コンポーネント

import { Link } from 'expo-router';

// シンプルなリンク
<Link href="/settings">Settings</Link>

// パラメータ付き
<Link href={{ pathname: '/player/[id]', params: { id: '123' } }}>
  View Player
</Link>

// ボタンとして
<Link href="/schedule" asChild>
  <Pressable>
    <Text>View Schedule</Text>
  </Pressable>
</Link>

// push の代わりに replace
<Link href="/home" replace>Home</Link>

スタックナビゲーション

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

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen 
        name="modal" 
        options={{ presentation: 'modal' }} 
      />
      <Stack.Screen 
        name="player/[id]" 
        options={{ 
          headerTitle: 'Player',
          headerBackTitle: 'Back',
        }} 
      />
    </Stack>
  );
}

動的なヘッダーオプション

// app/player/[id].tsx
import { Stack, useLocalSearchParams } from 'expo-router';

export default function PlayerScreen() {
  const { id } = useLocalSearchParams();
  const player = usePlayer(id);

  return (
    <>
      <Stack.Screen 
        options={{ 
          headerTitle: player?.name ?? 'Loading...',
          headerRight: () => (
            <EditButton playerId={id} />
          ),
        }} 
      />
      <PlayerProfile player={player} />
    </>
  );
}

モーダル

// どこからでもモーダルとして表示
router.push('/booking-modal');

// app/booking-modal.tsx
import { Stack, useRouter } from 'expo-router';

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

  const handleComplete = () => {
    router.dismiss(); // or router.back()
  };

  return (
    <>
      <Stack.Screen 
        options={{ 
          presentation: 'modal',
          headerLeft: () => (
            <Button title="Cancel" onPress={() => router.dismiss()} />
          ),
        }} 
      />
      <BookingForm onComplete={handleComplete} />
    </>
  );
}

// _layout.tsx で、モーダル画面を設定します
<Stack.Screen 
  name="booking-modal" 
  options={{ 
    presentation: 'modal',
    headerShown: true,
  }} 
/>

ディープリンク

app.json で scheme を設定

{
  "expo": {
    "scheme": "myapp",
    "ios": {
      "bundleIdentifier": "com.yourcompany.myapp",
      "associatedDomains": ["applinks:yourdomain.com"]
    }
  }
}

ディープリンクをテスト

# iOS Simulator
npx uri-scheme open "myapp://player/123" --ios

# Physical device
npx expo start --dev-client
# その後、Safari で myapp://player/123 を開きます

ユニバーサルリンク (iOS)

  1. associatedDomains を app.json に追加します
  2. apple-app-site-association ファイルを https://yourdomain.com/.well-known/apple-app-site-association でホストします。
{
  "applinks": {
    "apps": [],
    "details": [{
      "appID": "TEAMID.com.yourcompany.myapp",
      "paths": ["/player/*", "/schedule/*"]
    }]
  }
}

受信リンクを処理


// app/_layout.tsx
import { useEffect } from 'react';
import * as Linking from 'expo-linking';
import { useRouter } from 'expo-router';

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

  useEffect(() => {
    // アプリを開いたリンクを処理
    Linking.getInitialURL().then((url) => {
      if (url) handleDeepLink(url);
    });

    // アプリが開いている間にリンクを処理
    const subscription = Linking.addEventListener('url', ({ url }) => {
      handleDeepLink(url);
    });

    return () => subscription.remove();

(原文はここで切り詰められています)
📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開

React Native Navigation (Expo Router)

File-Based Routing Fundamentals

Expo Router uses file-system based routing. Files in app/ become routes.

Route Structure

app/
├── _layout.tsx          # Root layout (providers, global UI)
├── index.tsx            # "/" route
├── (tabs)/              # Tab group (parentheses = layout group)
│   ├── _layout.tsx      # Tab bar configuration
│   ├── home.tsx         # "/home" tab
│   └── profile.tsx      # "/profile" tab
├── (auth)/              # Auth flow group
│   ├── _layout.tsx      # Auth-specific layout
│   ├── login.tsx        # "/login"
│   └── register.tsx     # "/register"
├── settings/
│   ├── index.tsx        # "/settings"
│   └── [id].tsx         # "/settings/123" (dynamic)
└── [...missing].tsx     # Catch-all 404

Layout Groups (groupName)

Parentheses create layout groups - they affect layout hierarchy but not URL:

// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';

export default function TabLayout() {
  return (
    <Tabs>
      <Tabs.Screen 
        name="home" 
        options={{ 
          title: 'Home',
          tabBarIcon: ({ color }) => <HomeIcon color={color} />,
        }} 
      />
      <Tabs.Screen 
        name="profile" 
        options={{ title: 'Profile' }} 
      />
    </Tabs>
  );
}

Dynamic Routes [param]

// app/player/[id].tsx
import { useLocalSearchParams } from 'expo-router';

export default function PlayerScreen() {
  const { id } = useLocalSearchParams<{ id: string }>();

  return <PlayerProfile playerId={id} />;
}

Catch-All Routes [...slug]

// app/[...missing].tsx
import { Link, Stack } from 'expo-router';

export default function NotFound() {
  return (
    <>
      <Stack.Screen options={{ title: 'Not Found' }} />
      <Link href="/">Go home</Link>
    </>
  );
}

Navigation Patterns

Programmatic Navigation

import { useRouter, Link } from 'expo-router';

function MyComponent() {
  const router = useRouter();

  // Navigate with push (adds to stack)
  router.push('/player/123');

  // Navigate with params
  router.push({
    pathname: '/player/[id]',
    params: { id: '123' },
  });

  // Replace current screen (no back)
  router.replace('/home');

  // Go back
  router.back();

  // Navigate to root
  router.navigate('/');

  // Dismiss modal
  router.dismiss();
}

Link Component

import { Link } from 'expo-router';

// Simple link
<Link href="/settings">Settings</Link>

// With params
<Link href={{ pathname: '/player/[id]', params: { id: '123' } }}>
  View Player
</Link>

// As button
<Link href="/schedule" asChild>
  <Pressable>
    <Text>View Schedule</Text>
  </Pressable>
</Link>

// Replace instead of push
<Link href="/home" replace>Home</Link>

Stack Navigation

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

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen 
        name="modal" 
        options={{ presentation: 'modal' }} 
      />
      <Stack.Screen 
        name="player/[id]" 
        options={{ 
          headerTitle: 'Player',
          headerBackTitle: 'Back',
        }} 
      />
    </Stack>
  );
}

Dynamic Header Options

// app/player/[id].tsx
import { Stack, useLocalSearchParams } from 'expo-router';

export default function PlayerScreen() {
  const { id } = useLocalSearchParams();
  const player = usePlayer(id);

  return (
    <>
      <Stack.Screen 
        options={{ 
          headerTitle: player?.name ?? 'Loading...',
          headerRight: () => (
            <EditButton playerId={id} />
          ),
        }} 
      />
      <PlayerProfile player={player} />
    </>
  );
}

Modals

// Present as modal from anywhere
router.push('/booking-modal');

// app/booking-modal.tsx
import { Stack, useRouter } from 'expo-router';

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

  const handleComplete = () => {
    router.dismiss(); // or router.back()
  };

  return (
    <>
      <Stack.Screen 
        options={{ 
          presentation: 'modal',
          headerLeft: () => (
            <Button title="Cancel" onPress={() => router.dismiss()} />
          ),
        }} 
      />
      <BookingForm onComplete={handleComplete} />
    </>
  );
}

// In _layout.tsx, configure the modal screen
<Stack.Screen 
  name="booking-modal" 
  options={{ 
    presentation: 'modal',
    headerShown: true,
  }} 
/>

Deep Linking

Configure scheme in app.json

{
  "expo": {
    "scheme": "myapp",
    "ios": {
      "bundleIdentifier": "com.yourcompany.myapp",
      "associatedDomains": ["applinks:yourdomain.com"]
    }
  }
}

Test deep links

# iOS Simulator
npx uri-scheme open "myapp://player/123" --ios

# Physical device
npx expo start --dev-client
# Then open myapp://player/123 in Safari

Universal Links (iOS)

  1. Add associatedDomains to app.json
  2. Host apple-app-site-association file at https://yourdomain.com/.well-known/apple-app-site-association:
{
  "applinks": {
    "apps": [],
    "details": [{
      "appID": "TEAMID.com.yourcompany.myapp",
      "paths": ["/player/*", "/schedule/*"]
    }]
  }
}

Handle incoming links

// app/_layout.tsx
import { useEffect } from 'react';
import * as Linking from 'expo-linking';
import { useRouter } from 'expo-router';

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

  useEffect(() => {
    // Handle link that opened the app
    Linking.getInitialURL().then((url) => {
      if (url) handleDeepLink(url);
    });

    // Handle links while app is open
    const subscription = Linking.addEventListener('url', ({ url }) => {
      handleDeepLink(url);
    });

    return () => subscription.remove();
  }, []);

  function handleDeepLink(url: string) {
    const { path, queryParams } = Linking.parse(url);
    // Expo Router handles most cases automatically
    // Custom logic here for special cases
  }

  return <Stack />;
}

Common Patterns

Auth-Protected Routes

See rn-auth skill for full auth context pattern. Key navigation piece:

// app/_layout.tsx
export default function RootLayout() {
  const { token, isLoading } = useAuth();
  const segments = useSegments();
  const router = useRouter();

  useEffect(() => {
    if (isLoading) return;

    const inAuthGroup = segments[0] === '(auth)';

    if (!token && !inAuthGroup) {
      router.replace('/(auth)/login');
    } else if (token && inAuthGroup) {
      router.replace('/(tabs)/home');
    }
  }, [token, isLoading]);

  return (
    <Stack>
      <Stack.Screen name="(auth)" options={{ headerShown: false }} />
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
    </Stack>
  );
}

Preventing Back Navigation

// After login success, replace to prevent back to login
router.replace('/(tabs)/home');

// For onboarding completion
router.replace('/home');

// In screen options
<Stack.Screen 
  name="checkout-complete" 
  options={{ 
    headerBackVisible: false,
    gestureEnabled: false, // Prevent swipe back
  }} 
/>

Passing Data Between Screens

// Option 1: URL params (simple data, survives refresh)
router.push({
  pathname: '/confirm',
  params: { date: '2025-01-15', courtId: '5' },
});

// Reading
const { date, courtId } = useLocalSearchParams();

// Option 2: Global state for complex data (doesn't survive refresh)
// Use context, zustand, or similar

Debugging Navigation

Log current route

import { usePathname, useSegments } from 'expo-router';

function DebugNav() {
  const pathname = usePathname();
  const segments = useSegments();

  console.log('Current path:', pathname);
  console.log('Segments:', segments);

  return null;
}

Common issues

Issue Solution
Screen not found Check file name matches route, check _layout.tsx includes screen
Tabs not showing Ensure tab screens are direct children of tab _layout.tsx
Back button missing Check headerShown in parent and child layouts
Deep link not working Verify scheme in app.json, test with uri-scheme CLI
Params undefined Use useLocalSearchParams not useSearchParams