react-native について
Expo、ナビゲーション、スタイリング、ネイティブコードを扱う開発者向けの React Native スキルです。実装パターンや例、モバイルアプリの一般的な作業向けリファレンスを提供します。
主な機能
- 01型付き関数コンポーネントのパターン
- 02Expo Router のナビゲーション例
- 03iOS と Android 固有コードの指針
- 04NativeWind のスタイリング例
- 05セーフエリアとキーボードの処理
活用シーン
- 01React Native アプリの構成
- 02Expo Router によるナビゲーション実装
- 03iOS と Android 向けレイアウトの調整
- 04NativeWind を使った画面構築
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ gentleman-programming/ gentleman-skills --skill react-nativeインストール後、Claude Code で/react-nativeを入力して読み込まれたか確認します。
詳細
使用するタイミング
次の場合にこのスキルを読み込みます:
- React Native でモバイルアプリケーションを構築する場合
- Expo のマネージドワークフローまたはベアワークフローを使用する場合
- React Navigation でナビゲーションを実装する場合
- NativeWind(RN 向け Tailwind)でスタイリングする場合
- プラットフォーム固有のコード(iOS/Android)を扱う場合
- ネイティブモジュールとリンクを管理する場合
重要なパターン
パターン 1:プロジェクト構成
src/
├── app/ # Expo Router screens (if using)
│ ├── (tabs)/ # Tab navigator group
│ ├── (auth)/ # Auth flow group
│ └── _layout.tsx # Root layout
├── components/
│ ├── ui/ # Reusable UI components
│ └── features/ # Feature-specific components
├── hooks/ # Custom hooks
├── services/ # API and external services
├── stores/ # State management (Zustand)
├── utils/ # Utility functions
├── constants/ # App constants, themes
└── types/ # TypeScript types
パターン 2:TypeScript を使用した関数コンポーネント
常に、適切な型を指定した関数コンポーネントを使用します。
import { View, Text, Pressable } from 'react-native';
import type { ViewStyle, TextStyle } from 'react-native';
interface ButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
export function Button({
title,
onPress,
variant = 'primary',
disabled = false
}: ButtonProps) {
return (
<Pressable
onPress={onPress}
disabled={disabled}
style={({ pressed }) => [
styles.button,
variant === 'secondary' && styles.buttonSecondary,
pressed && styles.buttonPressed,
disabled && styles.buttonDisabled,
]}
>
<Text style={styles.buttonText}>{title}</Text>
</Pressable>
);
}
パターン 3:プラットフォーム固有のコード
プラットフォーム固有のコードには、Platform モジュールまたはファイル拡張子を使用します。
import { Platform, StyleSheet } from 'react-native';
// Using Platform.select
const styles = StyleSheet.create({
container: {
paddingTop: Platform.select({
ios: 44,
android: 0,
}),
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
android: {
elevation: 4,
},
}),
},
});
// Or use file extensions:
// Component.ios.tsx
// Component.android.tsx
コード例
例 1:Expo Router のナビゲーション設定
// app/_layout.tsx
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
export default function RootLayout() {
return (
<>
<StatusBar style="auto" />
<Stack
screenOptions={{
headerShown: false,
animation: 'slide_from_right',
}}
>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="modal"
options={{
presentation: 'modal',
animation: 'slide_from_bottom',
}}
/>
</Stack>
</>
);
}
例 2:React Query を使用したカスタムフック
// hooks/useUser.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { userService } from '@/services/user';
import type { User, UpdateUserInput } from '@/types';
export function useUser(userId: string) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => userService.getById(userId),
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: UpdateUserInput) => userService.update(data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
},
});
}
例 3:NativeWind によるスタイリング
// With NativeWind (Tailwind for React Native)
import { View, Text, Pressable } from 'react-native';
import { styled } from 'nativewind';
const StyledPressable = styled(Pressable);
const StyledView = styled(View);
const StyledText = styled(Text);
export function Card({ title, description, onPress }: CardProps) {
return (
<StyledPressable
className="bg-white dark:bg-gray-800 rounded-2xl p-4 shadow-md active:scale-95"
onPress={onPress}
>
<StyledView className="flex-row items-center gap-3">
<StyledView className="w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-full items-center justify-center">
<StyledText className="text-blue-600 dark:text-blue-300 text-xl">
📱
</StyledText>
</StyledView>
<StyledView className="flex-1">
<StyledText className="text-lg font-semibold text-gray-900 dark:text-white">
{title}
</StyledText>
<StyledText className="text-sm text-gray-500 dark:text-gray-400">
{description}
</StyledText>
</StyledView>
</StyledView>
</StyledPressable>
);
}
例 4:セーフエリアとキーボードの処理
import { KeyboardAvoidingView, Platform } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
export function ScreenWrapper({ children }: { children: React.ReactNode }) {
return (
<SafeAreaView style={{ flex: 1 }} edges={['top', 'left', 'right']}>
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
>
{children}
</KeyboardAvoidingView>
</SafeAreaView>
);
}
例 5:永続化機能を備えた Zustand ストア
// stores/authStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface AuthState {
token: string | null;
user: User | null;
isAuthenticated: boolean;
login: (token: string, user: User) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
token: null,
user: null,
isAuthenticated: false,
login: (token, user) => set({ token, user, isAuthenticated: true }),
logout: () => set({ token: null, user: null, isAuthenticated: false }),
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
アンチパターン
避けるべきこと:あらゆる場所でインラインスタイルを使用する
// ❌ Bad - inline styles are hard to maintain and don't memoize
export function BadComponent() {
return (
<View style={{ flex: 1, padding: 16, backgroundColor: '#fff' }}>
<Text style={{ fontSize: 18, fontWeight: 'bold', color: '#333' }}>
Title
</Text>
</View>
);
}
// ✅ Good - use StyleSheet or NativeWind
const styles = StyleSheet.create({
container: { flex: 1, padding: 16, backgroundColor: '#fff' },
title: { fontSize: 18, fontWeight: 'bold', color: '#333' },
});
export function GoodComponent() {
return (
<View style={styles.container}>
<Text style={styles.title}>Title</Text>
</View>
);
}
避けるべきこと:あらゆる用途に TouchableOpacity を使用する
// ❌ Bad - TouchableOpacity is legacy
import { TouchableOpacity } from 'react-native';
// ✅ Good - Use Pressable with feedback
import { Pressable } from 'react-native';
<Pressable
onPress={onPress}
style={({ pressed }) => [
styles.button,
pressed && { opacity: 0.7 }
]}
>
{({ pressed }) => (
<Text style={pressed ? styles.textPressed : styles.text}>
Press Me
</Text>
)}
</Pressable>
避けるべきこと:読み込み状態とエラー状態への対応を忘れる
// ❌ Bad - no loading/error handling
export function UserProfile({ userId }: { userId: string }) {
const { data } = useUser(userId);
return <Text>{data.name}</Text>; // Will crash if data is undefined
}
// ✅ Good - handle all states
export function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = useUser(userId);
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
if (!data) return null;
return <Text>{data.name}</Text>;
}
クイックリファレンス
| タスク | パターン |
|---|---|
| 新しい Expo プロジェクトを作成する | npx create-expo-app@latest --template tabs |
| NativeWind を追加する | npx expo install nativewind tailwindcss |
| プラットフォームを確認する | Platform.OS === 'ios' |
| セーフエリアのインセット | useSafeAreaInsets() from react-native-safe-area-context |
| ナビゲーション | Expo Router で router.push('/screen') |
| ディープリンク | app.json の expo.scheme で設定する |
| 環境変数 | expo-constants または react-native-dotenv を使用する |
| アイコン | @expo/vector-icons(Expo に含まれています) |
| アニメーション | 60fps のアニメーションには react-native-reanimated を使用する |
| ジェスチャー | react-native-gesture-handler |
リソース
コメント
Markdown 対応