「React Nativeのボイラープレートを書くのが大変」「iOSとAndroid両方のプラットフォーム対応に時間がかかる」——モバイルアプリ開発の現場で、多くのエンジニアが感じている課題です。
OpenAI Codex CLIとReact Nativeを組み合わせることで、クロスプラットフォームのモバイルアプリ開発を大幅に効率化できます。コンポーネント生成からナビゲーション設計、ネイティブモジュール統合まで、AIが開発を加速します。
- Codex CLIでReact Nativeコンポーネントを自動生成し、開発速度を3倍に
- Expo Router・React Navigation・状態管理の設計をAIが最適化
- SES案件で需要の高いモバイルアプリ開発スキルが効率的に習得可能
Codex CLI × React Native開発の概要

React Nativeは、JavaScriptとReactでiOSとAndroidの両方に対応するネイティブアプリを開発できるフレームワークです。2026年現在、Expo SDKの成熟やReact Native New Architecture(Fabric + TurboModules)の安定化により、企業での採用が加速しています。
OpenAI Codex CLIと組み合わせることで、以下のメリットが得られます:
- コンポーネントの自動生成 — UIコンポーネントをプロンプトから一括作成
- ナビゲーション設計の最適化 — 画面遷移の設計とコード生成を自動化
- API連携の効率化 — REST/GraphQL連携のボイラープレートを自動生成
- プラットフォーム固有コードの管理 — iOS/Android差分の自動処理
- Codex CLIを使ったReact Nativeプロジェクトの初期セットアップ
- UIコンポーネント・画面・ナビゲーションの自動生成
- 状態管理・API連携・認証機能の実装方法
- SES現場でのモバイル開発案件の動向と活用法
プロジェクトの初期セットアップ
Expo + Codex CLIでの環境構築
2026年現在、React Native開発ではExpoの利用が主流です。Codex CLIで最適な初期構成を生成しましょう。
codex "Expo SDK 52でReact Nativeプロジェクトを作成して。
要件:
- TypeScript
- Expo Router(ファイルベースルーティング)
- Zustand(状態管理)
- React Query(データフェッチング)
- NativeWind v4(スタイリング)
- 認証機能(Expo AuthSession)
ディレクトリ構造も提案して"
Codex CLIが生成する構造:
my-app/
├── app/ # Expo Router ページ
│ ├── (auth)/
│ │ ├── login.tsx
│ │ └── register.tsx
│ ├── (tabs)/
│ │ ├── _layout.tsx
│ │ ├── index.tsx
│ │ ├── search.tsx
│ │ └── profile.tsx
│ ├── _layout.tsx # ルートレイアウト
│ └── +not-found.tsx
├── components/
│ ├── ui/ # 共通UIコンポーネント
│ │ ├── Button.tsx
│ │ ├── Card.tsx
│ │ ├── Input.tsx
│ │ └── Typography.tsx
│ └── features/ # 機能別コンポーネント
├── hooks/ # カスタムフック
├── stores/ # Zustandストア
├── services/ # API通信
├── utils/ # ユーティリティ
├── constants/ # 定数定義
├── types/ # 型定義
├── assets/ # 画像・フォント
├── app.json
├── tsconfig.json
├── tailwind.config.js
└── package.json
パッケージの一括インストール
codex "以下のパッケージを適切にインストールして、
設定ファイルも生成して:
- NativeWind v4 + Tailwind CSS
- Zustand + immer middleware
- @tanstack/react-query
- expo-secure-store(トークン保管)
- react-hook-form + zod(バリデーション)
- expo-image(最適化画像表示)"
UIコンポーネントの自動生成
再利用可能なコンポーネントライブラリ
codex "以下のUIコンポーネントをNativeWindスタイリングで作成して:
1. Button(primary/secondary/ghost variants, loading state)
2. Input(テキスト、パスワード、検索、テキストエリア)
3. Card(画像付き、アクション付き)
4. Avatar(画像、イニシャル、ステータスバッジ)
5. Badge(色バリエーション、カウント表示)
6. BottomSheet(react-native-reanimated使用)
各コンポーネントにPropsの型定義とStorybookストーリーも追加"
生成されるButton.tsxの例:
import { Pressable, ActivityIndicator } from 'react-native';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/utils/cn';
import { Typography } from './Typography';
const buttonVariants = cva(
'flex-row items-center justify-center rounded-xl',
{
variants: {
variant: {
primary: 'bg-blue-600 active:bg-blue-700',
secondary: 'bg-gray-100 active:bg-gray-200 border border-gray-300',
ghost: 'active:bg-gray-100',
destructive: 'bg-red-600 active:bg-red-700',
},
size: {
sm: 'h-9 px-3',
md: 'h-11 px-5',
lg: 'h-13 px-7',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
);
interface ButtonProps extends VariantProps<typeof buttonVariants> {
onPress: () => void;
children: string;
loading?: boolean;
disabled?: boolean;
icon?: React.ReactNode;
className?: string;
}
export function Button({
variant,
size,
onPress,
children,
loading,
disabled,
icon,
className,
}: ButtonProps) {
const textColor = variant === 'primary' || variant === 'destructive'
? 'text-white'
: 'text-gray-900';
return (
<Pressable
onPress={onPress}
disabled={disabled || loading}
className={cn(
buttonVariants({ variant, size }),
disabled && 'opacity-50',
className
)}
>
{loading ? (
<ActivityIndicator
color={variant === 'primary' ? '#fff' : '#333'}
size="small"
/>
) : (
<>
{icon && <>{icon}</>}
<Typography
className={cn(textColor, 'font-semibold', icon && 'ml-2')}
>
{children}
</Typography>
</>
)}
</Pressable>
);
}
リスト画面の自動生成
モバイルアプリで頻出するリスト表示画面を一括生成します。
codex "以下の要件でリスト画面を作成して:
- FlashList使用(高パフォーマンス)
- プルリフレッシュ対応
- 無限スクロール(ページネーション)
- スケルトンローディング
- 検索フィルター
- スワイプアクション(お気に入り、削除)
- 空状態の表示"
import { useState, useCallback } from 'react';
import { RefreshControl, View } from 'react-native';
import { FlashList } from '@shopify/flash-list';
import { useInfiniteQuery } from '@tanstack/react-query';
import { SearchInput } from '@/components/ui/Input';
import { ItemCard, ItemSkeleton } from '@/components/features/ItemCard';
import { EmptyState } from '@/components/ui/EmptyState';
import { SwipeableRow } from '@/components/ui/SwipeableRow';
import { fetchItems } from '@/services/api';
export default function ItemListScreen() {
const [searchQuery, setSearchQuery] = useState('');
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
refetch,
isRefetching,
} = useInfiniteQuery({
queryKey: ['items', searchQuery],
queryFn: ({ pageParam = 1 }) => fetchItems({ page: pageParam, q: searchQuery }),
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
initialPageParam: 1,
});
const items = data?.pages.flatMap((page) => page.items) ?? [];
const renderItem = useCallback(({ item }: { item: Item }) => (
<SwipeableRow
onFavorite={() => handleFavorite(item.id)}
onDelete={() => handleDelete(item.id)}
>
<ItemCard item={item} />
</SwipeableRow>
), []);
if (isLoading) {
return (
<View className="flex-1 p-4">
{Array.from({ length: 6 }).map((_, i) => (
<ItemSkeleton key={i} />
))}
</View>
);
}
return (
<View className="flex-1">
<SearchInput
value={searchQuery}
onChangeText={setSearchQuery}
placeholder="検索..."
className="mx-4 mt-4"
/>
<FlashList
data={items}
renderItem={renderItem}
estimatedItemSize={100}
onEndReached={() => hasNextPage && fetchNextPage()}
onEndReachedThreshold={0.5}
refreshControl={
<RefreshControl refreshing={isRefetching} onRefresh={refetch} />
}
ListEmptyComponent={
<EmptyState
icon="search"
title="データがありません"
description="条件を変えて再度検索してください"
/>
}
ListFooterComponent={
isFetchingNextPage ? <ItemSkeleton /> : null
}
/>
</View>
);
}
ナビゲーション設計の自動化
Expo Routerによるファイルベースルーティング
codex "以下の画面遷移を実装して:
1. 認証フロー: スプラッシュ → ログイン ↔ 登録 → メイン
2. タブナビゲーション: ホーム / 検索 / 通知 / プロフィール
3. モーダル: 投稿作成、設定
4. スタック: 詳細画面、編集画面
5. ディープリンク対応
Expo Routerのファイルベースルーティングで実装"
// app/_layout.tsx - ルートレイアウト
import { useEffect } from 'react';
import { Stack } from 'expo-router';
import { useAuth } from '@/hooks/useAuth';
import { useRouter, useSegments } from 'expo-router';
export default function RootLayout() {
const { user, isLoading } = useAuth();
const segments = useSegments();
const router = useRouter();
useEffect(() => {
if (isLoading) return;
const inAuthGroup = segments[0] === '(auth)';
if (!user && !inAuthGroup) {
router.replace('/(auth)/login');
} else if (user && inAuthGroup) {
router.replace('/(tabs)');
}
}, [user, isLoading, segments]);
return (
<Stack>
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="modal"
options={{ presentation: 'modal', headerShown: false }}
/>
</Stack>
);
}
タブナビゲーションのカスタマイズ
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Home, Search, Bell, User } from 'lucide-react-native';
import { useNotificationCount } from '@/hooks/useNotifications';
export default function TabLayout() {
const notificationCount = useNotificationCount();
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: '#2563eb',
tabBarInactiveTintColor: '#9ca3af',
tabBarStyle: {
borderTopWidth: 0,
elevation: 8,
shadowColor: '#000',
shadowOpacity: 0.1,
shadowRadius: 8,
},
}}
>
<Tabs.Screen
name="index"
options={{
title: 'ホーム',
tabBarIcon: ({ color, size }) => <Home size={size} color={color} />,
}}
/>
<Tabs.Screen
name="search"
options={{
title: '検索',
tabBarIcon: ({ color, size }) => <Search size={size} color={color} />,
}}
/>
<Tabs.Screen
name="notifications"
options={{
title: '通知',
tabBarIcon: ({ color, size }) => <Bell size={size} color={color} />,
tabBarBadge: notificationCount > 0 ? notificationCount : undefined,
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'プロフィール',
tabBarIcon: ({ color, size }) => <User size={size} color={color} />,
}}
/>
</Tabs>
);
}
状態管理とAPI連携
Zustandストアの自動生成
codex "認証・ユーザープロフィール・お気に入り管理のZustandストアを作成して。
- persist middleware(expo-secure-store)
- immer middleware
- TypeScript型安全
- devtools対応"
// stores/authStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import * as SecureStore from 'expo-secure-store';
interface User {
id: string;
email: string;
name: string;
avatar?: string;
}
interface AuthState {
user: User | null;
token: string | null;
isAuthenticated: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
updateProfile: (updates: Partial<User>) => void;
}
const secureStorage = createJSONStorage(() => ({
getItem: async (key: string) => {
return await SecureStore.getItemAsync(key);
},
setItem: async (key: string, value: string) => {
await SecureStore.setItemAsync(key, value);
},
removeItem: async (key: string) => {
await SecureStore.deleteItemAsync(key);
},
}));
export const useAuthStore = create<AuthState>()(
persist(
immer((set) => ({
user: null,
token: null,
isAuthenticated: false,
login: async (email, password) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const data = await response.json();
set((state) => {
state.user = data.user;
state.token = data.token;
state.isAuthenticated = true;
});
},
logout: () => {
set((state) => {
state.user = null;
state.token = null;
state.isAuthenticated = false;
});
},
updateProfile: (updates) => {
set((state) => {
if (state.user) {
Object.assign(state.user, updates);
}
});
},
})),
{
name: 'auth-storage',
storage: secureStorage,
partialize: (state) => ({
token: state.token,
user: state.user,
}),
}
)
);
React Queryによるデータフェッチング
codex "React QueryのカスタムフックをCRUD操作用に生成して。
- 楽観的更新(Optimistic Updates)
- エラーリトライ
- オフラインサポート
- TypeScript型安全"
// hooks/useItems.ts
import {
useQuery,
useMutation,
useQueryClient,
useInfiniteQuery,
} from '@tanstack/react-query';
import { apiClient } from '@/services/apiClient';
import type { Item, CreateItemInput, UpdateItemInput } from '@/types';
// 一覧取得
export function useItems(params?: { category?: string }) {
return useInfiniteQuery({
queryKey: ['items', params],
queryFn: ({ pageParam = 1 }) =>
apiClient.get<{ items: Item[]; nextPage: number | null }>(
`/items?page=${pageParam}&category=${params?.category ?? ''}`
),
getNextPageParam: (lastPage) => lastPage.nextPage,
initialPageParam: 1,
staleTime: 5 * 60 * 1000,
retry: 3,
});
}
// 詳細取得
export function useItem(id: string) {
return useQuery({
queryKey: ['items', id],
queryFn: () => apiClient.get<Item>(`/items/${id}`),
enabled: !!id,
});
}
// 作成(楽観的更新)
export function useCreateItem() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (input: CreateItemInput) =>
apiClient.post<Item>('/items', input),
onMutate: async (newItem) => {
await queryClient.cancelQueries({ queryKey: ['items'] });
const previous = queryClient.getQueryData(['items']);
// 楽観的に追加
queryClient.setQueryData(['items'], (old: any) => ({
...old,
pages: old?.pages?.map((page: any, i: number) =>
i === 0
? { ...page, items: [{ ...newItem, id: 'temp' }, ...page.items] }
: page
),
}));
return { previous };
},
onError: (_err, _newItem, context) => {
queryClient.setQueryData(['items'], context?.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['items'] });
},
});
}
// 削除(楽観的更新)
export function useDeleteItem() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => apiClient.delete(`/items/${id}`),
onMutate: async (id) => {
await queryClient.cancelQueries({ queryKey: ['items'] });
const previous = queryClient.getQueryData(['items']);
queryClient.setQueryData(['items'], (old: any) => ({
...old,
pages: old?.pages?.map((page: any) => ({
...page,
items: page.items.filter((item: Item) => item.id !== id),
})),
}));
return { previous };
},
onError: (_err, _id, context) => {
queryClient.setQueryData(['items'], context?.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['items'] });
},
});
}
プッシュ通知とディープリンク
プッシュ通知の実装
codex "Expo Notificationsでプッシュ通知を実装して:
- 通知許可のリクエスト
- FCMトークンの取得・サーバー送信
- フォアグラウンド/バックグラウンド通知ハンドリング
- 通知タップ時のディープリンク遷移"
// hooks/useNotifications.ts
import { useEffect, useRef, useState } from 'react';
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { useRouter } from 'expo-router';
import { Platform } from 'react-native';
import { apiClient } from '@/services/apiClient';
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
export function useNotificationSetup() {
const router = useRouter();
const notificationListener = useRef<Notifications.Subscription>();
const responseListener = useRef<Notifications.Subscription>();
useEffect(() => {
registerForPushNotifications();
// フォアグラウンド通知の受信
notificationListener.current =
Notifications.addNotificationReceivedListener((notification) => {
console.log('通知受信:', notification);
});
// 通知タップ時のハンドリング
responseListener.current =
Notifications.addNotificationResponseReceivedListener((response) => {
const data = response.notification.request.content.data;
if (data?.url) {
router.push(data.url as string);
}
});
return () => {
notificationListener.current?.remove();
responseListener.current?.remove();
};
}, []);
}
async function registerForPushNotifications() {
if (!Device.isDevice) {
console.log('シミュレーターでは通知が使えません');
return;
}
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') return;
const token = (
await Notifications.getExpoPushTokenAsync({
projectId: 'your-project-id',
})
).data;
// サーバーにトークンを送信
await apiClient.post('/users/push-token', { token });
if (Platform.OS === 'android') {
Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
});
}
}
パフォーマンス最適化
レンダリング最適化
codex "React Nativeのパフォーマンスを最適化して:
- 不要な再レンダリングの防止
- メモ化の適切な適用
- 画像の最適化(expo-image)
- アニメーションの最適化(Reanimated 3)
- リストのパフォーマンス改善"
// パフォーマンス最適化の例
import { memo, useMemo, useCallback } from 'react';
import { Image } from 'expo-image';
import Animated, {
useAnimatedStyle,
withSpring,
useSharedValue,
} from 'react-native-reanimated';
// memo化されたリストアイテム
const ListItem = memo(function ListItem({ item, onPress }: ListItemProps) {
const handlePress = useCallback(() => {
onPress(item.id);
}, [item.id, onPress]);
return (
<Pressable onPress={handlePress} className="flex-row p-4 border-b border-gray-100">
<Image
source={{ uri: item.imageUrl }}
style={{ width: 60, height: 60, borderRadius: 8 }}
contentFit="cover"
placeholder={item.blurhash}
transition={200}
cachePolicy="memory-disk"
/>
<View className="flex-1 ml-3 justify-center">
<Typography className="font-semibold">{item.title}</Typography>
<Typography className="text-gray-500 text-sm mt-1">{item.subtitle}</Typography>
</View>
</Pressable>
);
});
// スムーズなアニメーション
function AnimatedCard({ children, index }: { children: React.ReactNode; index: number }) {
const opacity = useSharedValue(0);
const translateY = useSharedValue(20);
useEffect(() => {
opacity.value = withSpring(1, { damping: 15 });
translateY.value = withSpring(0, { damping: 15, delay: index * 50 });
}, []);
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
transform: [{ translateY: translateY.value }],
}));
return <Animated.View style={animatedStyle}>{children}</Animated.View>;
}
アプリサイズの最適化
codex "React Nativeアプリのバンドルサイズを最適化して:
- 不要な依存関係の除去
- Tree shakingの確認
- 画像アセットの最適化
- Hermes JSエンジンの最適化設定
- EASビルドの設定"
| 最適化項目 | 効果 | 実装難易度 |
|---|---|---|
| Hermesエンジン | バンドルサイズ30%削減 | ★☆☆ |
| 画像圧縮(WebP変換) | アセットサイズ50%削減 | ★☆☆ |
| Tree shaking | 未使用コード除去 | ★★☆ |
| Code splitting | 初期ロード改善 | ★★☆ |
| ProGuard(Android) | APKサイズ20%削減 | ★★★ |
テスト戦略
Codex CLIでテスト一括生成
codex "以下のテストを生成して:
1. コンポーネントテスト(React Native Testing Library)
2. フックのテスト(renderHook)
3. ストアのテスト(Zustand)
4. ナビゲーションのテスト
5. E2Eテスト(Detox)"
// __tests__/components/Button.test.tsx
import { render, fireEvent, screen } from '@testing-library/react-native';
import { Button } from '@/components/ui/Button';
describe('Button', () => {
it('テキストが正しく表示される', () => {
render(<Button onPress={jest.fn()}>テスト</Button>);
expect(screen.getByText('テスト')).toBeTruthy();
});
it('タップでonPressが呼ばれる', () => {
const onPress = jest.fn();
render(<Button onPress={onPress}>タップ</Button>);
fireEvent.press(screen.getByText('タップ'));
expect(onPress).toHaveBeenCalledTimes(1);
});
it('loading中はActivityIndicatorが表示される', () => {
render(<Button onPress={jest.fn()} loading>ロード中</Button>);
expect(screen.queryByText('ロード中')).toBeNull();
expect(screen.getByTestId('activity-indicator')).toBeTruthy();
});
it('disabled時はタップが無効', () => {
const onPress = jest.fn();
render(<Button onPress={onPress} disabled>無効</Button>);
fireEvent.press(screen.getByText('無効'));
expect(onPress).not.toHaveBeenCalled();
});
});
SES現場での活用パターン
モバイルアプリ開発案件の動向
2026年のSES市場では、React Nativeによるモバイルアプリ開発案件が増加傾向にあります。
| 案件タイプ | 月単価相場 | 求められるスキル |
|---|---|---|
| React Native新規開発 | 70-90万円 | Expo, TypeScript, 状態管理 |
| 既存アプリのRN移行 | 75-95万円 | ネイティブ連携, パフォーマンス最適化 |
| クロスプラットフォーム保守 | 60-80万円 | iOS/Android両方の知識 |
| React Native + バックエンド | 80-100万円 | フルスタック, API設計 |
Codex CLIを活用した開発効率化
SES現場でCodex CLIを使うことで、以下の作業を大幅に効率化できます:
| 作業内容 | 従来の所要時間 | Codex CLI活用後 | 短縮率 |
|---|---|---|---|
| 画面実装(1画面) | 4-8時間 | 1-2時間 | 75%短縮 |
| APIクライアント生成 | 2-4時間 | 30分 | 80%短縮 |
| テスト生成 | 3-6時間 | 1時間 | 75%短縮 |
| ナビゲーション設計 | 2-3時間 | 30分 | 80%短縮 |
実践プロンプトテンプレート
新規画面の一括生成:
codex "以下の画面を実装して:
- APIエンドポイント: GET /api/products
- 一覧表示(FlashList、プルリフレッシュ、無限スクロール)
- 検索・フィルター機能
- 詳細画面(モーダル遷移)
- お気に入り登録(楽観的更新)
NativeWindでスタイリング、React Queryでデータ取得"
まとめ:Codex CLI × React Nativeで開発を加速する
OpenAI Codex CLIとReact Nativeの組み合わせは、モバイルアプリ開発を劇的に効率化します。
本記事のポイント:
- Expo + TypeScriptの最適な初期構成をAIが自動生成
- UIコンポーネント・ナビゲーション・状態管理の一括セットアップ
- React Query + Zustandでスケーラブルなデータ管理
- SES現場で高需要のモバイル開発スキルが効率的に習得可能
React Nativeの開発効率をCodex CLIで最大化し、クロスプラットフォーム開発者としての市場価値を高めましょう。