npx claudepluginhub sean-sunagaku/claude-code-plugin --plugin rn-debugThis skill uses the workspace's default tool permissions.
---
Build and maintain React Native apps with Expo SDK, EAS Build, EAS Update, and Continuous Native Generation. Use for configuring projects, adding native modules, building binaries, or OTA updates.
Provides React Native and Expo patterns for performant mobile apps, covering list optimization with FlatList/FlashList, Reanimated animations, navigation, UI, state management, and Expo workflows.
Sets up full Sentry SDK for error monitoring, tracing, profiling, session replay, and logging in React Native and Expo projects. Supports Expo managed, bare, and vanilla React Native.
Share bugs, ideas, or general feedback.
白い画面 or エラー
├─ 1. Metro ログを確認(import エラー? ネイティブモジュール?)
├─ 2. 初期 loading 状態が解消されるか確認(Auth 等)
├─ 3. ネイティブモジュールの問題か確認(Expo Go vs dev-client)
└─ 4. Provider のクラッシュを切り分け
firebase/auth と @firebase/auth を混在させると onAuthStateChanged が発火しないgrep -rn "@firebase/" src/
grep -rn "\"firebase/" src/
firebase/auth 等に統一することgetAuth() ではなく initializeAuth + getReactNativePersistence(AsyncStorage) が必要getAuth() フォールバック:function resolveAuth(): Auth {
try {
return initializeAuth(app, {
persistence: getReactNativePersistence(ReactNativeAsyncStorage)
});
} catch {
return getAuth(app); // ホットリロード時
}
}
RCTEventEmitter エラー → ネイティブモジュール未登録react-native-purchases (RevenueCat)expo-apple-authenticationreact-native-mapsnpx expo start --dev-client で起動することnpx expo prebuild → npx expo run:ios でリビルド127.0.0.1 を使う10.0.2.2 を使うlocalhost で動いており、上記アドレスでホストマシンに到達可能# エミュレータ Hub が動いているか
curl -s http://127.0.0.1:4400/emulators
# 各エミュレータの確認
curl -s http://127.0.0.1:9099/ # Auth
curl -s http://127.0.0.1:8080/ # Firestore
curl -s http://127.0.0.1:9199/ # Storage
experimentalForceLongPolling: true を設定const db = initializeFirestore(app, { experimentalForceLongPolling: true });
| 機能 | シミュレータ | 実機 |
|---|---|---|
| Apple Sign-In | 不可 | 可 |
| プッシュ通知 | 不可 | 可 |
| In-App Purchase | StoreKit Config 必要 | 可 |
| カメラ | 不可 | 可 |
| NFC | 不可 | 可 |
| Storage ACL(エミュレータ) | 無視(動作は正常) | 正常 |
eas build --local で作った IPA/APK が起動直後にクラッシュするFirebaseError: auth/invalid-api-key や TurboModule 変換エラーが出るextra.firebase が {} (空) になっているeas build --local は一時ディレクトリ(/var/folders/.../eas-build-local-nodejs/.../build/mobile)にプロジェクトをコピーして実行する。.env は通常 .gitignore 対象なのでコピーに含まれず、app.config.ts 実行時に process.env.EXPO_PUBLIC_* が全て undefined になる。
# ビルドログから extra の中身を確認
rg '"extra"' .tmp_eas_preview_build_*.log
# → "firebase": {} なら .env が読まれていない
.easignore は .gitignore と同じ書式だが、EAS ビルド専用のファイル除外設定。.gitignore で .env を除外していても、.easignore に .env を書かなければ EAS ビルドには含まれる。
重要: .easignore はリポジトリルートに置く。 EAS はアップロード tarball をリポジトリルートから作成する(project/mobile/... の形で梱包)。mobile/.easignore に置いても無視される。
# リポジトリルート/.easignore の例
node_modules/
.git/
tmp/
ios/
android/
# macOS メタデータ / エディタ一時ファイル(必須)
**/._*
**/*.swp
**/*.swo
# .env を明示的に含める(最後に書く)
!<app-dir>/.env
注意:
.easignore が存在しない場合は .gitignore がそのまま使われるため、.env が除外される.easignore を明示的に作成すること<app-dir>/ がある場合)では !<app-dir>/.env のようにルートからの相対パスで指定する**/._* と **/*.swp を必ず除外する(後述 Part 6 参照).easignore が使えない場合や確実性を上げたい場合:
{
"build:preview": "bash -lc 'set -a; [ -f .env ] && source .env; set +a; eas build --profile preview --platform ios --local'"
}
source .env だけではシェル変数止まりで子プロセス(eas build)に渡らないset -a で source した変数を自動的に 環境変数として export するset +a で自動 export を解除万が一 env が空のまま起動しても即クラッシュしないよう、resolveFirebaseConfig() でバリデーション → ダミー config フォールバック → console.warn で警告を出す防御コードを入れる。
eas build --local の PREPARE_PROJECT フェーズで tar 展開エラーが出るFailed to restore metadata: File exists のようなメッセージmacOS のメタデータファイル(._*)やエディタの swap ファイル(*.swp)がプロジェクトに残っていると、EAS が作成する project.tar.gz に混入する。tar 展開時にメタデータファイルと本体ファイルが衝突してエラーになる。
例:
app/(tabs)/._layout.tsx.swp ← これが混入
app/(tabs)/_layout.tsx ← 本体と衝突
# プロジェクト内の不要ファイルを検索
find . -name '._*' -o -name '*.swp' -o -name '*.swo' | head -20
find . -name '._*' -delete
find . -name '*.swp' -delete
find . -name '*.swo' -delete
.easignore と .gitignore の両方に除外ルールを追加:**/._*
**/*.swp
**/*.swo
シミュレータの動作確認に MobileMCP を活用する:
mobile_take_screenshot — 画面状態を視覚的に確認mobile_list_elements_on_screen — 要素の座標・ラベルを取得mobile_click_on_screen_at_coordinates — ボタンタップ等の操作mobile_type_keys — テキスト入力mobile_swipe_on_screen — スクロール操作// === DEBUG: レンダリング検出 ===
const renderCount = React.useRef(0);
renderCount.current += 1;
console.log(`[RENDER] ComponentName #${renderCount.current}`, { props });
// === DEBUG: Props変化追跡 ===
const prevProps = React.useRef(props);
React.useEffect(() => {
const changed = Object.entries(props).filter(
([key, val]) => prevProps.current[key as keyof typeof props] !== val
);
if (changed.length > 0) {
console.log('[PROPS CHANGED] ComponentName:', changed.map(([k]) => k));
}
prevProps.current = props;
});
React.useEffect(() => {
console.log('[EFFECT] effectName fired', { dep1, dep2 });
return () => console.log('[EFFECT CLEANUP] effectName');
}, [dep1, dep2]);
const value = { key1, key2 };
console.log('[CONTEXT] ProviderName value identity changed?',
Object.is(prevValue.current, value));
React.useEffect(() => {
console.log('[LISTENER] registering', collectionPath);
const unsub = onSnapshot(query, (snap) => {
console.log('[LISTENER] snapshot received', snap.size, 'docs');
});
return () => { console.log('[LISTENER] unsubscribing', collectionPath); unsub(); };
}, [deps]);
| プレフィックス | 用途 |
|---|---|
[RENDER] | コンポーネントレンダリング |
[PROPS CHANGED] | props の変化 |
[STATE] | state の変化 |
[EFFECT] | useEffect 実行 |
[EFFECT CLEANUP] | useEffect クリーンアップ |
[CONTEXT] | Context value 変化 |
[LISTENER] | リスナー登録/解除 |
[HOOK] | カスタムフック呼び出し |
[MEMO] | useMemo/useCallback の再計算 |
// === DEBUG: で始めて === で終わるコメントで囲む(grep で一括除去可能)grep -rn "// === DEBUG" src/ app/
詳細は references/rerender-causes.md を参照。