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.
下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。
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
$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. 下の青いボタンを押して
rn-navigation.zipをダウンロード - 2. ZIPファイルをダブルクリックで解凍 →
rn-navigationフォルダができる - 3. そのフォルダを
C:\Users\あなたの名前\.claude\skills\(Win)または~/.claude/skills/(Mac)へ移動 - 4. Claude Code を再起動
⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。
🎯 このSkillでできること
下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。
📦 インストール方法 (3ステップ)
- 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
- 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
- 3. 展開してできたフォルダを、ホームフォルダの
.claude/skills/に置く- · macOS / Linux:
~/.claude/skills/ - · Windows:
%USERPROFILE%\.claude\skills\
- · macOS / Linux:
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)
associatedDomainsを app.json に追加します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)
- Add
associatedDomainsto app.json - Host
apple-app-site-associationfile athttps://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 |