react-native について
React Native と Expo の開発者向けに、パフォーマンス重視のパターンをまとめています。リスト描画、アニメーション、ナビゲーション、UI、状態管理、モノレポでの依存関係配置を優先度順に扱います。
主な機能
- 01FlashList とメモ化されたリスト項目
- 02安定したコールバックと画像サイズ指定
- 03GPU に適したアニメーションパターン
- 04ネイティブナビゲーションと UI コンポーネント
- 05セレクターによる状態購読
- 06モノレポの依存関係配置ガイド
活用シーン
- 01大規模リストのスクロール改善
- 02アニメーションのカクつき軽減
- 03ネイティブナビゲーションとメニューの選択
- 04状態変化による不要な再レンダリングの抑制
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ s-hiraoku/ synapse-a2a --skill react-nativeインストール後、Claude Code で/react-nativeを入力して読み込まれたか確認します。
詳細
React Native
パフォーマンスを最優先した React Native と Expo のパターンを、影響度順にまとめています。
優先度 1:リストのパフォーマンス(重大)
リストは RN アプリで最も大きなパフォーマンスのボトルネックです。まずはここを適切に設定しましょう。
FlashList を使用する
大規模なリストでは、FlatList を @shopify/flash-list に置き換えます。
import { FlashList } from '@shopify/flash-list';
<FlashList
data={items}
renderItem={({ item }) => <ItemRow item={item} />}
estimatedItemSize={80}
keyExtractor={(item) => item.id}
/>
リスト項目をメモ化する
すべてのリスト項目をメモ化する必要があります。
const ItemRow = memo(function ItemRow({ item }: { item: Item }) {
return (
<View style={styles.row}>
<Text>{item.title}</Text>
</View>
);
});
コールバックを安定させる
コールバックを切り出し、リスト項目内でのインラインオブジェクトを避けます。
// BAD: New function + new style object every render
<Pressable onPress={() => onSelect(item.id)} style={{ padding: 16 }}>
// GOOD: Stable references
const handlePress = useCallback(() => onSelect(item.id), [item.id, onSelect]);
<Pressable onPress={handlePress} style={styles.pressable}>
リスト内の画像を最適化する
適切なサイズ設定とキャッシュを使って expo-image を使用します。
import { Image } from 'expo-image';
<Image
source={{ uri: item.thumbnailUrl }}
style={styles.thumbnail}
contentFit="cover"
placeholder={item.blurhash}
transition={200}
recyclingKey={item.id}
/>
異種リストの項目タイプ
getItemType を使用して、FlashList がセルを効率よく再利用できるようにします。
<FlashList
data={mixedItems}
renderItem={renderItem}
getItemType={(item) => item.type} // 'header' | 'content' | 'ad'
estimatedItemSize={100}
/>
優先度 2:アニメーション(高)
GPU 専用のプロパティ
アニメーションさせるのは transform と opacity のみです。それ以外はすべてレイアウトを再計算します。
import Animated, { useAnimatedStyle, withSpring } from 'react-native-reanimated';
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: withSpring(isPressed.value ? 0.95 : 1) }],
opacity: withSpring(isVisible.value ? 1 : 0),
}));
派生値
計算を重複させないよう、計算を伴うアニメーションには useDerivedValue を使用します。
const progress = useSharedValue(0);
const rotation = useDerivedValue(() => `${progress.value * 360}deg`);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ rotate: rotation.value }],
}));
ジェスチャー処理
60 fps でジェスチャーを追跡するには react-native-gesture-handler を使用します。
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
const pan = Gesture.Pan()
.onUpdate((e) => {
translateX.value = e.translationX;
translateY.value = e.translationY;
})
.onEnd(() => {
translateX.value = withSpring(0);
translateY.value = withSpring(0);
});
// Use Gesture.Tap() instead of Pressable for animated press feedback
const tap = Gesture.Tap()
.onBegin(() => { scale.value = withSpring(0.95); })
.onFinalize(() => { scale.value = withSpring(1); });
優先度 3:ナビゲーション(高)
ネイティブナビゲーター
JavaScript ベースの代替手段より、常にネイティブのスタックとタブを優先します。
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
// BAD: JS-based stack (slower transitions, no native gestures)
import { createStackNavigator } from '@react-navigation/stack';
// GOOD: Native stack (native transitions + gestures)
const Stack = createNativeStackNavigator();
画面オプション
ヘッダーとアニメーションをネイティブで設定します。
<Stack.Screen
name="Detail"
component={DetailScreen}
options={{
headerLargeTitle: true, // iOS large title
animation: 'slide_from_right',
}}
/>
優先度 4:UI パターン(高)
セーフエリア
あらゆるデバイスの形状に対して、セーフエリアを適切に処理します。
import { SafeAreaView } from 'react-native-safe-area-context';
// For scrollable content
<SafeAreaView edges={['top']} style={{ flex: 1 }}>
<ScrollView contentInsetAdjustmentBehavior="automatic">
{children}
</ScrollView>
</SafeAreaView>
ネイティブモーダル
JavaScript のオーバーレイではなく、ネイティブのモーダル表示を使用します。
<Stack.Screen
name="Settings"
component={SettingsScreen}
options={{ presentation: 'modal' }}
/>
ネイティブメニュー
カスタムのドロップダウンコンポーネントではなく、コンテキストメニューを使用します。
import * as ContextMenu from 'zeego/context-menu';
<ContextMenu.Root>
<ContextMenu.Trigger>
<Pressable><Text>Options</Text></Pressable>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Item key="edit" onSelect={handleEdit}>
<ContextMenu.ItemTitle>Edit</ContextMenu.ItemTitle>
</ContextMenu.Item>
<ContextMenu.Item key="delete" onSelect={handleDelete} destructive>
<ContextMenu.ItemTitle>Delete</ContextMenu.ItemTitle>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root>
TouchableOpacity より Pressable を優先する
// BAD: Legacy touch component
<TouchableOpacity onPress={onPress}>{children}</TouchableOpacity>
// GOOD: Modern Pressable with feedback
<Pressable
onPress={onPress}
style={({ pressed }) => [styles.button, pressed && styles.pressed]}
android_ripple={{ color: 'rgba(0,0,0,0.1)' }}
>
{children}
</Pressable>
優先度 5:状態管理(中)
再レンダリングを最小限にする
必要な状態のみを購読します。
// BAD: Re-renders on any store change
const store = useStore();
return <Text>{store.user.name}</Text>;
// GOOD: Selector extracts only needed value
const name = useStore((s) => s.user.name);
return <Text>{name}</Text>;
React Compiler との互換性
Reanimated と React Compiler を使用する場合:
// Destructure shared value functions for compiler compatibility
const { value } = useSharedValue(0);
// Use worklet directive for Reanimated callbacks
const animatedStyle = useAnimatedStyle(() => {
'worklet';
return { opacity: value };
});
優先度 6:モノレポ(中)
ネイティブ依存関係
ネイティブ依存関係は共有パッケージではなく、アプリパッケージに配置します。
packages/
ui/ # Pure React components (no native deps)
shared/ # Business logic, types
apps/
mobile/ # Native deps (expo-image, reanimated) here
依存関係のバージョンを統一する
モノレポ全体で、依存関係ごとに 1 つのバージョンのみを使用するようにします。
// Root package.json
{
"resolutions": {
"react-native": "0.76.x",
"react-native-reanimated": "3.x"
}
}
クイックリファレンス
| 問題 | 解決策 | 優先度 |
|---|---|---|
| スクロールが遅いリスト | FlashList + メモ化した項目 | 重大 |
| リスト内のインラインオブジェクト | StyleSheet に切り出す | 重大 |
| カクつくアニメーション | transform/opacity のみを使用 | 高 |
| JavaScript ベースのナビゲーション | ネイティブのスタック/タブ | 高 |
| カスタムのドロップダウンメニュー | ネイティブのコンテキストメニュー | 高 |
| ストア全体の購読 | セレクター | 中 |
| 共有パッケージ内のネイティブ依存関係 | アプリパッケージに移動する | 中 |