RORK LABEN
TOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しましたTOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しました
記事一覧/開発ツール
開発ツール/2026-05-24中級

Rork アプリでバックグラウンド復帰時に画面が真っ白/状態が消える原因の切り分け

Rork で作ったアプリをバックグラウンドに置いたまま数時間放置して戻ると、画面が真っ白になったり、編集途中のデータが消えていたり、再ログインを求められたりする。AppState・AsyncStorage 復元タイミング・iOS のメモリ回収・Navigation state 復元の4つの観点から切り分ける手順を、5,000万DL の壁紙・癒し系アプリを個人開発してきた経験を交えてまとめます。

rork58react-native12appstatebackgroundtroubleshooting25

Rork アプリでバックグラウンド復帰時に画面が真っ白/状態が消える原因の切り分け

アーティスト・クリエイターの廣川政樹です。Rork で作ったアプリを TestFlight に上げて自分の端末で1日使い、翌朝起きてアプリに戻ると画面が真っ白だった。あるいは、編集途中のメモやお気に入りに登録した壁紙が消えていた。こうした「バックグラウンド復帰時の事故」は、シミュレーターでは絶対に再現しないため、原因の特定がいちばん難しい部類のトラブルです。

私は 2014年から個人でアプリ開発を続けており、累計 5,000万DL を超える壁紙・癒し系アプリを今も AdMob 収益でまわしています。同じ症状をこれまで何度も踏み抜いてきたので、自分用のチェックリストを共有します。原因は大きく4つに分かれ、それぞれ別の場所で対処しないと永遠に再発します。

まず切り分けるべき4つの状態

「バックグラウンドから戻ったらおかしい」という症状は、実は内部で起きていることが全く違います。最初に4つのどれなのかを区別することが、解決までの最短距離です。

症状主な原因対処カテゴリ
真っ白で操作できないプロセス再起動でバンドル/state が未復元iOS の再起動扱い
画面は表示されるがログイン切れSecureStore は残るが Auth 状態が初期化AppState 復帰時の再認証
入力途中の文字や下書きが消えたコンポーネント state はメモリ依存・永続化なし下書き永続化の追加
ナビゲーションが Tab1 に戻るNavigationContainer の state 復元未設定Navigation 永続化の追加

iOS は OS 側の判断でいつでもバックグラウンドのアプリプロセスを終了します。Apple のドキュメントが言う「Suspended → Terminated」が起きると、戻ってきた時はコールドスタートと同じ扱いになります。Android はメモリ余裕があれば JS の実行コンテキスト自体は生きていることが多く、症状の出方が違います。iOS で出るのか、Android で出るのか、両方なのかを最初にメモすることが大事です。

AppState が active に戻った時に何が起きているか

react-nativeAppStateactive / background / inactive の3状態を返します。プロセスが完全に殺されていなければ、復帰時に backgroundactive のリスナーが発火します。逆に、リスナーがまったく発火せず JS バンドルのロードからやり直しになる場合は「プロセスごと再起動した」と判定できます。

切り分けの第一歩として、AppState の遷移をログに残す最小コードを入れておきます。

// src/lib/appstate-logger.tsx
import { AppState, AppStateStatus } from "react-native";
import { useEffect } from "react";
import * as FileSystem from "expo-file-system";
 
const LOG_FILE = FileSystem.documentDirectory + "appstate.log";
 
export function useAppStateLogger() {
  useEffect(() => {
    const handle = async (next: AppStateStatus) => {
      const line = `${new Date().toISOString()} ${next}\n`;
      try {
        await FileSystem.writeAsStringAsync(LOG_FILE, line, {
          encoding: FileSystem.EncodingType.UTF8,
          // 追記モード相当: 既存内容を読んで連結
        });
      } catch (e) {
        console.warn("appstate log failed", e);
      }
    };
    const sub = AppState.addEventListener("change", handle);
    handle(AppState.currentState);
    return () => sub.remove();
  }, []);
}

このフックをルートレイアウトで一度だけ呼んでおき、症状が出た端末で appstate.log を取り出すと「リスナーが発火していない=コールドスタート」と「発火しているのに UI が真っ白」がはっきり分かれます。私の場合、5,000万DL の壁紙アプリで最も多かったのは前者でした。iOS で6時間以上バックグラウンドに置かれた端末では、ほぼ確実にプロセスが回収されています。

ケース1: iOS によるプロセス回収 — コールドスタート前提で書く

iOS のメモリ回収は防ぎようがありません。バックグラウンドに置けば必ず殺される可能性があるという前提で書く、というのが対策のすべてです。

具体的には次の3つを守ります。

  1. 起動時に必要な永続データは すべて AsyncStorage / SecureStore / SQLite に書く(メモリだけに置かない)
  2. JS のグローバル変数や Zustand のメモリ store は「キャッシュ」と割り切る
  3. スプラッシュ画面は復元処理が完了してから手動で非表示にする

スプラッシュの制御は expo-splash-screenpreventAutoHideAsync で止めて、復元処理の完了後に hideAsync する形が安全です。

// src/app/_layout.tsx
import { useEffect, useState } from "react";
import * as SplashScreen from "expo-splash-screen";
import { restoreAppState } from "@/lib/restore";
 
SplashScreen.preventAutoHideAsync().catch(() => {});
 
export default function RootLayout() {
  const [ready, setReady] = useState(false);
 
  useEffect(() => {
    (async () => {
      try {
        await restoreAppState(); // AsyncStorage + Auth 復元
      } finally {
        setReady(true);
        await SplashScreen.hideAsync();
      }
    })();
  }, []);
 
  if (!ready) return null;
  return <AppNavigator />;
}

readyfalse の間に null を返すのがポイントです。よくあるのは、復元処理が走る前に「ログイン画面」と「ホーム画面」のどちらに行くかを判定してしまい、未復元の null トークンを見て常にログイン画面に流す、というバグです。これが「バックグラウンド復帰したらログアウトしている」の正体であることが多いです。

ケース2: 入力途中のデータが消える — 下書き自動保存の最小実装

コンポーネント state は揮発性なので、メモやコメントなど「ユーザーが書き途中の文字」は秒単位で AsyncStorage に逃がしておく必要があります。私が癒し系アプリのジャーナル機能で使っているのは、debounce 付きの最小実装です。

// src/hooks/useDraft.ts
import { useEffect, useRef, useState } from "react";
import AsyncStorage from "@react-native-async-storage/async-storage";
 
export function useDraft(key: string, initial = "") {
  const [value, setValue] = useState(initial);
  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
 
  // 初回マウントで復元
  useEffect(() => {
    AsyncStorage.getItem(key).then((v) => {
      if (v !== null) setValue(v);
    });
  }, [key]);
 
  // 500ms debounce で保存
  useEffect(() => {
    if (timer.current) clearTimeout(timer.current);
    timer.current = setTimeout(() => {
      AsyncStorage.setItem(key, value).catch(() => {});
    }, 500);
    return () => {
      if (timer.current) clearTimeout(timer.current);
    };
  }, [key, value]);
 
  const clear = async () => {
    await AsyncStorage.removeItem(key);
    setValue("");
  };
 
  return [value, setValue, clear] as const;
}

注意点として、AppStatebackground に遷移した瞬間に 即座に書く処理 も追加しておくと安全です。iOS は background 通知後に数秒で Suspended に入るため、debounce の500ms が間に合わないことがあります。

useEffect(() => {
  const sub = AppState.addEventListener("change", (s) => {
    if (s === "background" && timer.current) {
      clearTimeout(timer.current);
      AsyncStorage.setItem(key, value).catch(() => {});
    }
  });
  return () => sub.remove();
}, [key, value]);

ケース3: ナビゲーションが必ず Tab1 に戻る — state 永続化の設定

React Navigation の NavigationContainer は、デフォルトでは state を永続化しません。プロセス再起動するたびに最初のスクリーンに戻ります。これも「バックグラウンドから戻ったら別の画面に飛ばされた」と感じる主な理由のひとつです。

initialState / onStateChange を使った最小の永続化はこうなります。

// src/navigation/PersistentNavigationContainer.tsx
import { useEffect, useState } from "react";
import { NavigationContainer, NavigationState } from "@react-navigation/native";
import AsyncStorage from "@react-native-async-storage/async-storage";
 
const PERSISTENCE_KEY = "NAV_STATE_V1";
 
export function PersistentNavigationContainer({ children }: { children: React.ReactNode }) {
  const [isReady, setIsReady] = useState(false);
  const [initialState, setInitialState] = useState<NavigationState | undefined>();
 
  useEffect(() => {
    const restore = async () => {
      try {
        const saved = await AsyncStorage.getItem(PERSISTENCE_KEY);
        if (saved) setInitialState(JSON.parse(saved));
      } catch {}
      setIsReady(true);
    };
    restore();
  }, []);
 
  if (!isReady) return null;
 
  return (
    <NavigationContainer
      initialState={initialState}
      onStateChange={(state) => {
        AsyncStorage.setItem(PERSISTENCE_KEY, JSON.stringify(state)).catch(() => {});
      }}
    >
      {children}
    </NavigationContainer>
  );
}

PERSISTENCE_KEY にバージョン番号を入れているのは意図的です。Navigation の構造(タブ名・スクリーン名)を変更した時にキーを上げないと、古い state を復元してクラッシュします。アプリの審査リジェクト経由でこれを学びました。

expo-router を使っている場合は、現時点では state の手動永続化 API が公式に提供されていません。代わりに、深いリンク(deep link)として最後のパスを AsyncStorage に保存しておき、起動時に router.replace(savedPath) する戦略が現実的です。

ケース4: メモリ警告で Zustand が消える — 永続化ミドルウェアを足す

ZustandJotai の store はメモリ上の値なので、プロセス再起動で必ず消えます。これは設計通りの挙動ですが、ユーザーから見ると「お気に入りが消えた」「設定がリセットされた」になります。

Zustand なら persist ミドルウェアを足すだけで解決します。

// src/store/favorites.ts
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import AsyncStorage from "@react-native-async-storage/async-storage";
 
type FavoritesState = {
  ids: string[];
  toggle: (id: string) => void;
};
 
export const useFavorites = create<FavoritesState>()(
  persist(
    (set, get) => ({
      ids: [],
      toggle: (id) => {
        const cur = get().ids;
        set({ ids: cur.includes(id) ? cur.filter((x) => x !== id) : [...cur, id] });
      },
    }),
    {
      name: "favorites-v1",
      storage: createJSONStorage(() => AsyncStorage),
      version: 1,
    }
  )
);

ハマりやすいのは、復元前に画面が描画されて空配列のまま表示されるパターンです。useFavorites.persist.hasHydrated()_layout.tsx 側で待ち、復元が終わってから子コンポーネントを描画する形にすると安全です。

再現環境を手元に作る

最後に、これらの問題を出社前のシミュレーターで再現する方法です。バックグラウンド復帰の事故は端末でないと出ないと信じられがちですが、iOS シミュレーターでも Device → Erase All Content and Settings を経由せずに再現できます。

  • iOS シミュレーター: アプリをホームに送ったあと、xcrun simctl terminate booted <bundle-id> でプロセスだけ落とす。次にアプリを開くとプロセス再起動になる
  • Android エミュレーター: adb shell am force-stop <package-name> で同じことができる

これを CI ではなく日常の手動テストに組み込むだけで、リリース前に大半の事故を潰せます。私は新機能を入れるたびに、terminate → 再起動 → 復元できているか、というチェックを5パターンほどルーチン化しています。

バックグラウンド復帰の事故は派手なクラッシュとして表に出ないので、ユーザーは静かに離脱します。レビュー欄に「久しぶりに開いたらデータが全部消えていた」と書かれて初めて気付く、というのが最悪のシナリオです。AppState・永続化・Navigation・スプラッシュの4点を最初に整えておけば、長く運用する個人開発アプリにとって最も静かなトラブル源を一つ減らせます。

お読みいただきありがとうございました。同じ症状で苦戦している方の切り分けの一助になれば嬉しいです。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

開発ツール2026-05-23
Rork 生成プロジェクト特有の expo start --offline forbidden — テンプレ構成が原因の4パターン
Rork が生成するテンプレート構成が原因で expo start --offline が forbidden になる4つのパターン(tsconfigPaths・expo-router キャッシュ・ネイティブプリビルド・lock 不一致)を、Rork プロジェクト固有の観点で切り分けます。汎用的な Expo 側の対処(プロキシ403・依存検証)は別記事にまとめています。
開発ツール2026-03-26
Rork アプリの起動クラッシュ・白画面 完全解決ガイド
Rorkアプリが起動直後にクラッシュしたり、白画面のまま固まる問題を根本から解決。白画面・即座クラッシュ・遅延クラッシュ・リリルビドのクラッシュなど5パターンの原因と対処法を網羅。Xcode・Logcat・React Native Debuggerでの実際のデバッグ手順付き。
開発ツール2026-03-26
Rork React Native ビルドエラー完全解決ガイド 2026
Rork で生成した React Native アプリのビルドエラーを完全解決。Metro Bundler、ネイティブモジュールリンク、Gradle、Xcode、Expo 設定エラーまで、症状・原因・解決策を網羅しています。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →