mcpField
R
react-native

react-native

github.com/travisjneuman開発107153 回インストール今日更新
リポジトリ

react-native について

React Nativeアプリ開発者向けの実践ガイドです。ExpoとReact Native CLI、ナビゲーション、スタイル、ネイティブ機能に加え、新アーキテクチャ、Expo Router、EAS更新、性能と配布を扱います。

主な機能

  1. 01ExpoとReact Native CLIの比較
  2. 02ナビゲーション、ルーティング、スタイルの設定
  3. 03カメラ、通知、位置情報の利用
  4. 04Fabric、TurboModules、JSIの解説
  5. 05EASビルドとOTA更新の設定
  6. 06リストとHermesランタイムの最適化

活用シーン

  1. 01iOS・Androidアプリのプロジェクト作成
  2. 02アプリへのナビゲーションとネイティブ機能の追加
  3. 03アプリのビルドとOTA更新の設定
  4. 04モバイルアプリのリスト性能改善

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/travisjneuman/.claude --skill react-native

インストール後、Claude Code で/react-nativeを入力して読み込まれたか確認します。

詳細

React Native 開発

React Native と Expo で、ネイティブの iOS アプリと Android アプリを構築します。

フレームワークの選択肢

フレームワーク使用する場面
Expo(推奨)ほとんどのアプリ、高速な開発、マネージドワークフロー
React Native CLIカスタムネイティブモジュールが必要な場合、既存アプリへの統合
Expo with Dev Client両方の利点を活用 — ネイティブモジュールに対応した Expo DX

プロジェクトのセットアップ

Expo(推奨)
npx create-expo-app@latest my-app
cd my-app
npx expo start
React Native CLI
npx @react-native-community/cli init MyApp
cd MyApp
npx react-native run-ios  # or run-android

コアコンポーネント

基本構造
import { View, Text, StyleSheet, ScrollView, SafeAreaView } from "react-native";

export default function App() {
  return (
    <SafeAreaView style={styles.container}>
      <ScrollView contentContainerStyle={styles.content}>
        <Text style={styles.title}>Hello World</Text>
      </ScrollView>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
  },
  content: {
    padding: 16,
  },
  title: {
    fontSize: 24,
    fontWeight: "bold",
  },
});
一般的なコンポーネント
import {
  View,
  Text,
  Image,
  TextInput,
  TouchableOpacity,
  Pressable,
  FlatList,
  ActivityIndicator,
  Modal,
  Switch,
} from 'react-native';

// Touchable with feedback
<Pressable
  onPress={handlePress}
  style={({ pressed }) => [
    styles.button,
    pressed && styles.buttonPressed,
  ]}
>
  <Text>Press Me</Text>
</Pressable>

// Text Input
<TextInput
  value={text}
  onChangeText={setText}
  placeholder="Enter text"
  style={styles.input}
  autoCapitalize="none"
  keyboardType="email-address"
  returnKeyType="done"
  onSubmitEditing={handleSubmit}
/>

// Image
<Image
  source={{ uri: 'https://example.com/image.jpg' }}
  style={{ width: 100, height: 100 }}
  resizeMode="cover"
/>

ナビゲーション

React Navigation のセットアップ
npm install @react-navigation/native @react-navigation/native-stack
npx expo install react-native-screens react-native-safe-area-context
スタックナビゲーション
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";

type RootStackParamList = {
  Home: undefined;
  Details: { itemId: string };
};

const Stack = createNativeStackNavigator<RootStackParamList>();

export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        <Stack.Screen
          name="Home"
          component={HomeScreen}
          options={{ title: "My App" }}
        />
        <Stack.Screen
          name="Details"
          component={DetailsScreen}
          options={({ route }) => ({ title: route.params.itemId })}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

// Navigate
navigation.navigate("Details", { itemId: "123" });

// Go back
navigation.goBack();
タブナビゲーション
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { Ionicons } from "@expo/vector-icons";

const Tab = createBottomTabNavigator();

function TabNavigator() {
  return (
    <Tab.Navigator
      screenOptions={({ route }) => ({
        tabBarIcon: ({ focused, color, size }) => {
          const iconName =
            route.name === "Home"
              ? focused
                ? "home"
                : "home-outline"
              : focused
                ? "settings"
                : "settings-outline";
          return <Ionicons name={iconName} size={size} color={color} />;
        },
      })}
    >
      <Tab.Screen name="Home" component={HomeScreen} />
      <Tab.Screen name="Settings" component={SettingsScreen} />
    </Tab.Navigator>
  );
}

状態管理

Zustand(推奨)
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import AsyncStorage from "@react-native-async-storage/async-storage";

interface AuthStore {
  user: User | null;
  token: string | null;
  login: (user: User, token: string) => void;
  logout: () => void;
}

export const useAuthStore = create<AuthStore>()(
  persist(
    (set) => ({
      user: null,
      token: null,
      login: (user, token) => set({ user, token }),
      logout: () => set({ user: null, token: null }),
    }),
    {
      name: "auth-storage",
      storage: createJSONStorage(() => AsyncStorage),
    },
  ),
);
React Query
import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from "@tanstack/react-query";

const queryClient = new QueryClient();

// Wrap app
<QueryClientProvider client={queryClient}>
  <App />
</QueryClientProvider>;

// Use in component
function ItemList() {
  const { data, isLoading, error, refetch } = useQuery({
    queryKey: ["items"],
    queryFn: fetchItems,
  });

  if (isLoading) return <ActivityIndicator />;
  if (error) return <Text>Error: {error.message}</Text>;

  return (
    <FlatList
      data={data}
      renderItem={({ item }) => <ItemRow item={item} />}
      keyExtractor={(item) => item.id}
      onRefresh={refetch}
      refreshing={isLoading}
    />
  );
}

スタイリング

StyleSheet
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#ffffff",
  },
  row: {
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "space-between",
    padding: 16,
    borderBottomWidth: StyleSheet.hairlineWidth,
    borderBottomColor: "#ccc",
  },
  text: {
    fontSize: 16,
    color: "#333",
  },
  shadow: {
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3, // Android
  },
});
NativeWind(RN 向け Tailwind)
npm install nativewind tailwindcss
import { Text, View } from "react-native";

export function Card() {
  return (
    <View className="bg-white rounded-lg p-4 shadow-md">
      <Text className="text-lg font-bold text-gray-900">Card Title</Text>
      <Text className="text-gray-600 mt-2">Card content goes here</Text>
    </View>
  );
}

ネイティブ機能

カメラ(Expo)
import { Camera, CameraType } from "expo-camera";

function CameraScreen() {
  const [permission, requestPermission] = Camera.useCameraPermissions();
  const cameraRef = useRef<Camera>(null);

  const takePicture = async () => {
    if (cameraRef.current) {
      const photo = await cameraRef.current.takePictureAsync();
      console.log(photo.uri);
    }
  };

  if (!permission?.granted) {
    return <Button title="Grant Permission" onPress={requestPermission} />;
  }

  return (
    <Camera ref={cameraRef} style={styles.camera} type={CameraType.back}>
      <TouchableOpacity onPress={takePicture}>
        <Text>Take Photo</Text>
      </TouchableOpacity>
    </Camera>
  );
}
プッシュ通知(Expo)
import * as Notifications from "expo-notifications";
import { useEffect } from "react";

Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: false,
  }),
});

async function registerForPushNotifications() {
  const { status } = await Notifications.requestPermissionsAsync();
  if (status !== "granted") return;

  const token = await Notifications.getExpoPushTokenAsync();
  return token.data;
}

// Listen for notifications
useEffect(() => {
  const subscription = Notifications.addNotificationReceivedListener(
    (notification) => {
      console.log(notification);
    },
  );
  return () => subscription.remove();
}, []);
位置情報
import * as Location from "expo-location";

async function getLocation() {
  const { status } = await Location.requestForegroundPermissionsAsync();
  if (status !== "granted") return;

  const location = await Location.getCurrentPositionAsync({});
  return {
    latitude: location.coords.latitude,
    longitude: location.coords.longitude,
  };
}

パフォーマンス

FlatList の最適化
<FlatList
  data={items}
  renderItem={renderItem}
  keyExtractor={(item) => item.id}
  // Performance optimizations
  initialNumToRender={10}
  maxToRenderPerBatch={10}
  windowSize={5}
  removeClippedSubviews={true}
  getItemLayout={(data, index) => ({
    length: ITEM_HEIGHT,
    offset: ITEM_HEIGHT * index,
    index,
  })}
/>;

// Memoize render item
const renderItem = useCallback(
  ({ item }: { item: Item }) => <MemoizedItem item={item} />,
  [],
);

const MemoizedItem = memo(({ item }: { item: Item }) => (
  <View>
    <Text>{item.name}</Text>
  </View>
));
画像の最適化
import { Image } from "expo-image";

<Image
  source={{ uri: imageUrl }}
  style={{ width: 200, height: 200 }}
  contentFit="cover"
  placeholder={blurhash}
  transition={200}
  cachePolicy="memory-disk"
/>;

テスト

Jest + React Native Testing Library
import { render, fireEvent, waitFor } from "@testing-library/react-native";
import { LoginScreen } from "./LoginScreen";

describe("LoginScreen", () => {
  it("should login successfully", async () => {
    const onLogin = jest.fn();
    const { getByPlaceholderText, getByText } = render(
      <LoginScreen onLogin={onLogin} />,
    );

    fireEvent.changeText(getByPlaceholderText("Email"), "[email protected]");
    fireEvent.changeText(getByPlaceholderText("Password"), "password123");
    fireEvent.press(getByText("Login"));

    await waitFor(() => {
      expect(onLogin).toHaveBeenCalledWith({
        email: "[email protected]",
        password: "password123",
      });
    });
  });
});

アプリストアへのデプロイ

Expo EAS Build
# Install EAS CLI
npm install -g eas-cli

# Configure
eas build:configure

# Build for iOS
eas build --platform ios

# Build for Android
eas build --platform android

# Submit to stores
eas submit --platform ios
eas submit --platform android
app.json の設定
{
  "expo": {
    "name": "My App",
    "slug": "my-app",
    "version": "1.0.0",
    "ios": {
      "bundleIdentifier": "com.mycompany.myapp",
      "buildNumber": "1"
    },
    "android": {
      "package": "com.mycompany.myapp",
      "versionCode": 1
    }
  }
}

ベストプラクティス

推奨事項:
  • 開発を高速化するために Expo を使用する
  • 適切な TypeScript の型を実装する
  • 大規模なリストには FlashList を使用する
  • キーボードを適切に処理する
  • 実機でテストする
避けるべきこと:
  • render 内でインラインスタイルを使う
  • リストのメモ化を省略する
  • プラットフォーム間の違いを無視する
  • 重い計算で JS スレッドをブロックする
  • ディープリンクの処理を忘れる

新アーキテクチャ(React Native 0.76+)

React Native の新アーキテクチャは、従来のブリッジを、より高性能で同期的なレイヤーに置き換えます。

コアコンポーネント
コンポーネント置き換えるもの利点
FabricPaper同期レンダリング、並行機能
TurboModulesNative Modules遅延読み込み、同期アクセス、型安全性
JSIBridgeJS とネイティブ間の直接通信、JSON 不要
Bridgeless ModeBridgeブリッジを完全に削除し、メモリ使用量を削減
新アーキテクチャを有効にする
// app.json (Expo)
{
  "expo": {
    "newArchEnabled": true
  }
}
# ios/Podfile (bare React Native)
ENV['RCT_NEW_ARCH_ENABLED'] = '1'
TurboModules(型安全なネイティブモジュール)
// NativeCalculator.ts
import type { TurboModule } from "react-native";
import { TurboModuleRegistry } from "react-native";

export interface Spec extends TurboModule {
  add(a: number, b: number): number; // Synchronous!
  fetchData(url: string): Promise<string>; // Async
}

export default TurboModuleRegistry.getEnforcing<Spec>("NativeCalculator");
Fabric コンポーネント(新しいレンダラー)

Fabric は、データ取得用の Suspense、トランジション、自動バッチ処理など、React 18/19 の並行レンダリング機能を有効にします。


Expo Router(ファイルベースのルーティング)

npx create-expo-app@latest --template tabs
ファイル構成
app/
├── _layout.tsx          # Root layout
├── index.tsx            # Home screen (/)
├── (tabs)/              # Tab group
│   ├── _layout.tsx      # Tab navigator
│   ├── index.tsx        # First tab
│   └── settings.tsx     # Settings tab
├── [id].tsx             # Dynamic route (/123)
├── modal.tsx            # Modal screen
└── +not-found.tsx       # 404 screen
レイアウトとナビゲーション
// 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>
  );
}

// Navigate with type-safe links
import { Link, router } from "expo-router";

<Link href="/settings">Settings</Link>
<Link href={{ pathname: "/[id]", params: { id: "123" } }}>Details</Link>

// Programmatic navigation
router.push("/settings");
router.replace("/login");
router.back();

React 19 との互換性

React Native では、新アーキテクチャを使用すると React 19 の機能がサポートされます。

  • use() フックで Promise とコンテキストを読み取る
  • ref を prop として使用(forwardRef は不要)
  • 自動メモ化のための React Compiler サポート
  • フォーム処理向けの useActionState と useOptimistic

EAS Update(OTA アップデート)

# Install EAS CLI
npm install -g eas-cli

# Configure updates
eas update:configure

# Push an OTA update (no app store review)
eas update --branch production --message "Bug fix for login"
eas update --branch preview --message "New feature preview"
// Check for updates in app
import * as Updates from "expo-updates";

async function checkForUpdates() {
  const update = await Updates.checkForUpdateAsync();
  if (update.isAvailable) {
    await Updates.fetchUpdateAsync();
    await Updates.reloadAsync(); // Apply update
  }
}

Hermes エンジン

Hermes は React Native のデフォルトの JS エンジンで、モバイル向けに最適化されています。

  • バイトコードの事前コンパイルによる 起動の高速化
  • JavaScriptCore より 低いメモリ使用量
  • Chrome DevTools Protocol による デバッグの改善
// app.json - Hermes is enabled by default in Expo SDK 52+
{
  "expo": {
    "jsEngine": "hermes"
  }
}

Expo SDK 52 以降の機能

機能説明
Expo Router v4API ルートに対応したファイルベースのルーティング
expo-camera (next)バーコードスキャンに対応したカメラ API
expo-videoexpo-av に代わる最新の動画プレーヤー
expo-sqliteJSI を介した同期 API に対応する SQLite
DOM ComponentsReact Native 内で Web コンポーネントをレンダリング
React 19 supportNew Architecture による React 19 の全機能への対応
Universal linksapp.json で設定するディープリンク

コメント