RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Articles/Dev Tools
Dev Tools/2026-05-24Intermediate

Why your Rork app shows a blank screen or loses state after returning from background

Your Rork app sits in the background overnight, you tap the icon the next morning, and the screen is blank — or your drafts have disappeared, or you have been silently logged out. iOS process reclamation, AsyncStorage rehydration timing, and Navigation state restoration each cause a different version of this bug. Notes from running wallpaper and wellness apps with 50M downloads as an indie developer.

rork58react-native12appstatebackgroundtroubleshooting65

Why your Rork app shows a blank screen or loses state after returning from background

I am Masaki Hirokawa, an artist and indie developer. You ship your Rork app to TestFlight, use it for a day, leave it in the background overnight, and the next morning it opens to a blank screen. Or your in-progress notes are gone. Or your saved wallpapers vanished. These "background resume" failures are some of the hardest bugs to chase because the iOS simulator almost never reproduces them.

I have been building mobile apps since 2014 and currently run a portfolio of wallpaper and wellness apps that have crossed 50 million downloads, monetized mostly through AdMob. I have walked into this trap many times, so here is the checklist I keep next to my desk. The root cause is almost always one of four things, and each one needs to be fixed in a different place.

Four distinct symptoms hide under one complaint

"My app is broken after coming back from background" is shorthand for four very different problems. Knowing which one you have is the fastest path to a fix.

SymptomMost likely causeWhere to fix
Blank, unresponsive screenProcess killed, bundle/state not restoredCold-start path
Visible UI but user is logged outSecureStore is fine, auth state was in memoryAppState resume handler
Draft text or unsaved input goneComponent state is volatile, no persistenceAdd draft persistence
Always lands back on Tab 1NavigationContainer state not persistedAdd navigation persistence

iOS reclaims background app processes whenever it wants. Apple's documentation calls this "Suspended → Terminated", and once it happens, your next launch is a cold start. Android keeps the JS context alive more often when memory is available, so the symptoms differ. The first thing to write down is does this happen on iOS, Android, or both?

What actually happens when AppState becomes active

AppState from react-native reports active, background, and inactive. If the process is still alive, the listener fires when the user returns. If the listener never fires and you watch JS bundles reload from scratch, the OS killed your process.

A minimal AppState logger I drop into every project:

// 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();
  }, []);
}

Call this hook once in your root layout, then pull appstate.log from a device showing the bug. The distinction between "listener never fired = cold start" and "listener fired but UI is blank" is the single most useful piece of information you can collect. In my wallpaper app catalog with 50M total downloads, the cold-start case dominates on iPhones left untouched for six hours or more.

Case 1: iOS killed the process — write for cold start by default

You cannot prevent iOS from reclaiming a backgrounded process. The only durable fix is to write every screen as if it might launch from scratch.

Three rules to follow:

  1. Anything required to render the first screen must live in AsyncStorage / SecureStore / SQLite, never only in JS memory.
  2. Treat in-memory Zustand or global variables as a cache, not a source of truth.
  3. Keep the splash screen up until restoration finishes — never let an unrestored render flash.

expo-splash-screen handles step 3 cleanly:

// 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 restore
      } finally {
        setReady(true);
        await SplashScreen.hideAsync();
      }
    })();
  }, []);
 
  if (!ready) return null;
  return <AppNavigator />;
}

Returning null while ready is false is the critical part. The most common bug I see is code that decides between "login screen" and "home screen" before the token has been pulled from SecureStore, sees a null token, and sends every returning user back to login. That is the real reason your app "always logs me out after I leave it overnight".

Case 2: drafts disappear — minimal autosave

Component state is volatile. Anything the user typed — notes, comments, search queries — needs to be mirrored to AsyncStorage on a tight cadence. Here is the debounced draft hook I use in my journaling app:

// 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]);
 
  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;
}

Add a synchronous flush on background transition, since iOS may suspend you within seconds and the 500 ms debounce can lose its last write:

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]);

Case 3: navigation always lands back on Tab 1 — persist the nav state

NavigationContainer does not persist state by default. After a process restart, your user lands on the first registered screen. To users this reads as "the app forgot where I was".

Minimal manual persistence:

// 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>
  );
}

The _V1 suffix on the key is intentional. Any change to your tab or screen structure should bump the version, otherwise a stale saved state can restore a screen that no longer exists and crash on launch. I learned this through an App Store rejection — funniest review note I have ever received.

For expo-router projects there is no official manual persistence API yet. A pragmatic substitute is to save the last known path to AsyncStorage and router.replace(savedPath) once your restoration is done.

Case 4: memory warnings clear Zustand — add the persist middleware

Zustand and Jotai stores live in memory and disappear on every process restart. That is by design, but to a user it looks like "my favorites were wiped".

Zustand fixes this with one middleware:

// 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,
    }
  )
);

The trap is rendering children before hydration completes — the user sees an empty list for a half second. Use useFavorites.persist.hasHydrated() at the root and gate the render until it returns true.

Reproducing the bug on a simulator

Background-resume bugs are easy to dismiss as "only happens on real devices", but you can reproduce most of them deterministically without erasing the simulator:

  • iOS Simulator: send the app to the home screen, then run xcrun simctl terminate booted <bundle-id> to kill only the process. Opening the app afterwards triggers a full cold start.
  • Android Emulator: adb shell am force-stop <package-name> does the same.

Wire this into manual QA before every release. I run this 5-step check on every new feature in my apps: terminate → relaunch → confirm restoration → terminate again from a deep screen → confirm restoration. It catches almost all of these issues before they reach a real user.

Background-resume failures rarely surface as loud crashes — users just quietly stop opening the app. The worst version of this story is reading "lost everything when I came back" in a review weeks later. Getting AppState handling, persistence, navigation restoration, and splash gating right at the start removes one of the quietest, most damaging sources of churn for a long-lived indie app.

Thanks for reading — I hope this helps you isolate your version of the bug faster than I did.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-05-23
Rork-Specific 'expo start --offline forbidden': Four Causes in Rork's Template Config
When expo start --offline returns 'forbidden' specifically on a Rork-generated project, the cause is usually Rork template config: tsconfigPaths, an un-generated expo-router cache, native prebuild, or a lockfile mismatch. Four Rork-specific fixes; the generic Expo proxy and dependency-validation guide is covered separately.
Dev Tools2026-03-26
Rork App Launch Crashes & White Screen: Complete Debugging Guide
Fix app crashes and white screen errors in Rork apps. Five crash patterns with debugging steps: boot white screen, instant crash, delayed crash, release-build crashes, and device-specific issues. Includes Xcode, Logcat, and React Native Debugger techniques.
Dev Tools2026-03-26
Rork React Native Build Errors: Complete Troubleshooting Guide
Master React Native build failures in Rork. From Metro crashes to Gradle and Xcode errors, learn the root causes and step-by-step fixes for every common scenario.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →