react-native について
React Native と Expo の開発者向けに、モバイルアプリの性能と設計パターンを影響度順にまとめたガイドです。リスト、アニメーション、ナビゲーション、Expo のワークフローを扱います。
主な機能
- 01影響度順のルール
- 02リストとスクロールの性能対策
- 03アニメーションとジェスチャーのパターン
- 04ナビゲーションと UI の指針
- 05状態、データ、メディアの指針
- 06Expo のワークフローとテスト
活用シーン
- 01React Native または Expo アプリの新規作成
- 02リストのスクロールの遅さの調査
- 03モバイルアプリの性能レビュー
- 04Expo プロジェクトとテストの設定
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill react-nativeインストール後、Claude Code で/react-nativeを入力して読み込まれたか確認します。
詳細
React Native のパターン
React Native + Expo アプリのパフォーマンスとアーキテクチャに関するパターンです。影響度の高い順にルールを並べています。MEDIUM に取り組む前に、CRITICAL を解決してください。
これは出発点です。モバイルアプリをさらに構築するにつれて、このスキルは成長していきます。
適用する場面
- 新しい React Native または Expo アプリを構築するとき
- リストやスクロールのパフォーマンスを最適化するとき
- アニメーションを実装するとき
- パフォーマンス上の問題がないかモバイルコードをレビューするとき
- 新しい Expo プロジェクトをセットアップするとき
1. リストのパフォーマンス(CRITICAL)
リストは、React Native におけるパフォーマンスの問題の第 1 位です。スクロールがカクつくと、アプリ全体の使用感が損なわれます。
| パターン | 問題 | 解決策 |
|---|---|---|
| データ表示に ScrollView を使用 | <ScrollView> はすべてのアイテムを一度に描画します | <FlatList> または <FlashList> を使用します。仮想化されており、表示中のアイテムだけを描画します |
| keyExtractor がない | keyExtractor のない FlatList は不要な再レンダリングが発生します | keyExtractor={(item) => item.id} を指定します。各アイテムに安定した一意のキーを設定します |
| 複雑な renderItem | renderItem 内の負荷の高いコンポーネントは、スクロールのたびに再レンダリングされます | React.memo でラップし、別のコンポーネントに切り出します |
| renderItem 内のインライン関数 | renderItem={({ item }) => <Row onPress={() => nav(item.id)} />} | ハンドラーを切り出します:const handlePress = useCallback(...) |
| getItemLayout がない | FlatList はスクロール時に各アイテムのサイズを測定します(負荷が高くなります) | 高さが固定のアイテムには getItemLayout を指定します:(data, index) => ({ length: 80, offset: 80 * index, index }) |
| FlashList | FlatList も優れていますが、大きなリストには FlashList がさらに適しています | @shopify/flash-list はドロップインで置き換えられ、リサイクル方式のアーキテクチャを採用しています |
| リスト内の大きな画像 | フル解像度の画像はメインスレッドでデコードされます | expo-image をプレースホルダーとトランジションとともに使用し、サイズを指定します |
FlatList のチェックリスト
すべての FlatList に次の項目を設定してください:
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={renderItem} // Memoised component
getItemLayout={getItemLayout} // If items are fixed height
initialNumToRender={10} // Don't render 100 items on mount
maxToRenderPerBatch={10} // Batch size for off-screen rendering
windowSize={5} // How many screens to keep in memory
removeClippedSubviews={true} // Unmount off-screen items (Android)
/>
2. アニメーション(HIGH)
ネイティブアニメーションは UI スレッドで実行されます。JS アニメーションは JS スレッドをブロックし、カクつきを引き起こします。
| パターン | 問題 | 解決策 |
|---|---|---|
| 複雑なアニメーションに Animated API を使用 | Animated は JS スレッドで実行され、操作を妨げます | UI スレッドで実行される react-native-reanimated を使用します |
| レイアウトアニメーション | アイテムがトランジションなしで表示・非表示になります | LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) |
| 共有要素のトランジション | 画面間を移動すると、要素が瞬間移動します | react-native-reanimated の共有トランジション、または expo-router の共有要素を使用します |
| ジェスチャー + アニメーション | ドラッグやスワイプが遅れて感じられます | react-native-gesture-handler と reanimated のワークレットを組み合わせます。すべて UI スレッドで実行されます |
| レイアウトの測定 | onLayout の発火が遅く、ちらつきが発生します | 共有値と useAnimatedStyle を使用して、即座に反応させます |
Reanimated の基本
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
function AnimatedBox() {
const offset = useSharedValue(0);
const style = useAnimatedStyle(() => ({
transform: [{ translateX: withSpring(offset.value) }],
}));
return (
<GestureDetector gesture={panGesture}>
<Animated.View style={[styles.box, style]} />
</GestureDetector>
);
}
3. ナビゲーション(HIGH)
| パターン | 問題 | 解決策 |
|---|---|---|
| Expo Router | React Native 向けのファイルベースルーティング(Next.js のような方式)です | _layout.tsx ファイルを含む app/ ディレクトリを使用します。新しい Expo プロジェクトで推奨されます |
| スタック内の負荷の高い画面 | すべての画面がスタック内でマウントされたままになります | 表示されていないときに保持する必要がない画面には unmountOnBlur: true を使用します |
| ディープリンク | アプリが URL に反応しません | Expo Router では自動的に処理されます。Bare RN の場合は Linking API の設定が必要です |
| タブバッジの更新 | タブにフォーカスがあってもバッジの数が更新されません | useIsFocused() を使用するか、フォーカス時に再取得します:useFocusEffect(useCallback(...)) |
| ナビゲーション状態の永続化 | アプリがバックグラウンドに移行したり終了したりすると、位置が失われます | AsyncStorage と組み合わせて onStateChange と initialState を使用します |
Expo Router の構成
app/
├── _layout.tsx # Root layout (tab navigator)
├── index.tsx # Home tab
├── (tabs)/
│ ├── _layout.tsx # Tab bar config
│ ├── home.tsx
│ ├── search.tsx
│ └── profile.tsx
├── [id].tsx # Dynamic route
└── modal.tsx # Modal route
4. UI パターン(高)
| パターン | 問題 | 解決策 |
|---|---|---|
| セーフエリア | ノッチやホームインジケーターの下にコンテンツが表示されます | react-native-safe-area-context の <SafeAreaView> または useSafeAreaInsets() |
| キーボード回避 | フォームのフィールドがキーボードに隠れます | <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'}> |
| プラットフォーム固有のコード | iOS と Android で異なる動作が必要です | Platform.select({ ios: ..., android: ... }) または .ios.tsx/.android.tsx ファイル |
| ステータスバー | ステータスバーがコンテンツに重なる、または色が正しくありません | ルートレイアウトで expo-status-bar の <StatusBar style="auto" /> を使用します |
| タッチ領域 | ボタンが小さすぎてタップできません | 最小サイズは 44x44pt です。hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} を使用します |
| 触覚フィードバック | タップしても反応が感じられません | expo-haptics を使用し、重要なアクションで Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light) を呼び出します |
5. 画像とメディア(中)
| パターン | 問題 | 解決策 |
|---|---|---|
| 画像コンポーネント | react-native の <Image> は基本的な機能しかありません | expo-image を使用します。キャッシュ、プレースホルダー、トランジション、blurhash に対応しています |
| 寸法が指定されていないリモート画像 | 画像の読み込み時にレイアウトがずれます | 必ず width と height を指定するか、aspectRatio を使用します |
| 大きな画像 | Android で OOM クラッシュが発生します | サーバー側でサイズを変更するか、メモリを適切に処理する expo-image を使用します |
| SVG | SVG は標準ではサポートされていません | SVG のインポートには react-native-svg と react-native-svg-transformer を使用します |
| 動画 | 動画の再生 | expo-av または expo-video(新しい API)を使用します |
6. 状態とデータ(中)
| パターン | 問題 | 解決策 |
|---|---|---|
| 複雑なデータに AsyncStorage を使用 | 読み込みのたびに JSON のパース/文字列化が発生します | MMKV(react-native-mmkv)を使用します。AsyncStorage より 30 倍高速です |
| グローバル状態 | 単純な状態管理に Redux/MobX のボイラープレートが必要です | Zustand は最小限の実装で、React Native との相性も優れています |
| サーバー状態 | フェッチ、読み込み中、エラー、キャッシュを手動で管理する必要があります | TanStack Query は Web 版と同様に React Native でも動作します |
| オフラインファースト | ネットワークがないとアプリを使えません | TanStack Query の persistQueryClient と MMKV を組み合わせるか、複雑なオフライン処理には WatermelonDB を使用します |
| 深い階層の状態更新 | ネストされたオブジェクトでスプレッド演算子が煩雑になります | Zustand 経由で Immer を使用します:set(produce(state => { state.user.name = 'new' })) |
7. Expo ワークフロー(中)
| パターン | 使用する場面 | 方法 |
|---|---|---|
| 開発ビルド | ネイティブモジュールが必要な場合 | npx expo run:ios または eas build --profile development |
| Expo Go | ネイティブモジュールを使わない素早いプロトタイピング | npx expo start を実行し、QR コードをスキャンします |
| EAS Build | CI/CD、アプリストア向けビルド | eas build --platform ios --profile production |
| EAS Update | アプリストアの審査なしでホットフィックスを適用する場合 | eas update --branch production --message "Fix bug" |
| 設定プラグイン | eject せずにネイティブ設定を変更する場合 | expo-build-properties またはカスタム設定プラグインを使い、app.config.ts を設定します |
| 環境変数 | ビルドごとに異なる設定を使う場合 | eas.json のビルドプロファイルと expo-constants を使用します |
新しいプロジェクトのセットアップ
npx create-expo-app my-app --template tabs
cd my-app
npx expo install expo-image react-native-reanimated react-native-gesture-handler react-native-safe-area-context
8. テスト(低〜中)
| ツール | 用途 | セットアップ |
|---|---|---|
| Jest | 単体テスト、フックのテスト | Expo にデフォルトで含まれています |
| React Native Testing Library | コンポーネントのテスト | @testing-library/react-native |
| Detox | 実機/シミュレーターでの E2E テスト | detox — Wix のテストフレームワーク |
| Maestro | YAML フローによる E2E テスト | maestro test flow.yaml — Detox より簡単です |
よくある落とし穴
| 落とし穴 | 対処方法 |
|---|---|
| Metro バンドラーのキャッシュ | npx expo start --clear |
| Pod のインストールに関する問題(iOS) | cd ios && pod install --repo-update |
| Reanimated が動作しない | ルートで最初にインポートする必要があります:import 'react-native-reanimated' |
| Expo SDK のアップグレード | SDK のバージョンを更新した後、npx expo install --fix を実行します |
| Android のビルドに失敗する | メモリ設定を gradle.properties で確認します:org.gradle.jvmargs=-Xmx4g |
| iOS シミュレーターの動作が遅い | パフォーマンステストには実機を使用してください。シミュレーターでは実際のパフォーマンスを再現できません |
コメント
Markdown 対応