CORE JSC

International Technology Partnership

Mobile Development

Fixing React Native Apps That Show a Blank Screen or Lose State After Android Kills Them in the Background

Switching away from the app for a while, then returning to it, occasionally shows a blank screen or dumps the user back at the very first screen with every bit of navigation state and form input gone — on some devices, consistently; on others, almost never. This isn't a background-transition bug; Android killed the entire process while it was backgrounded, and reopening it is actually a cold start the app's own code never noticed happening.

Core JSC Team·October 31, 2026
React NativeAndroidMobile DevelopmentState ManagementNavigation

The Problem

A user backgrounds the app — switches to another app, checks a notification, locks the screen for a while — and later returns to it by tapping its icon or switching back through the recent-apps view. Most of the time, the app resumes exactly where it was left. Occasionally, though, the same action instead shows a blank white or black screen for a moment, or the app appears to relaunch from its very first screen with the user's navigation stack, form inputs, and any in-memory state completely gone, as if the app had been freshly installed. The behavior is inconsistent — it happens more often on some devices, after longer background periods, or when several other apps were used in between.

Why It Happens

Android can terminate a backgrounded app's entire process to reclaim memory, independent of anything the app's own code does

Unlike simply moving to the background — which pauses the app but keeps its process and all in-memory state alive — Android's system can decide to kill a backgrounded app's process outright under memory pressure, to free resources for whatever's currently in the foreground. This is standard, expected platform behavior, not a bug or a misconfiguration, and it happens more aggressively on lower-memory devices or after many other apps have been opened in between.

Reopening the app after process death is a genuine cold start, even though it visually looks like resuming

When the user taps the app's icon (or its entry in recent apps) after the process was killed, Android creates an entirely new process and runs through the app's full startup sequence again — including React Native's JavaScript engine initializing from scratch. Any state that existed purely in memory — Redux/Zustand store contents, component state, an in-progress navigation stack — is gone, because none of it was ever written anywhere that survives a process being killed; the app's own code never received any signal that this specific restart is different from a normal first launch.

The blank screen specifically comes from Android's own activity-recreation step briefly preceding the JS engine being ready

Android's own native activity layer can be restored slightly before React Native's JavaScript bundle has finished initializing and rendered its first real screen, producing a brief window where the native shell exists but has nothing meaningful from the JS side to display yet. On a device where this gap is short, a user might never consciously notice it; on a slower device, or after a longer period away, that gap is wide enough to register as a distinct blank-screen moment before the app's actual UI appears.

The Fix

1. Persist the state that actually matters to the user's experience, rather than keeping it purely in memory

// Using a persistence layer for the relevant store slices
// (e.g. redux-persist, zustand's persist middleware, or manual AsyncStorage writes
// at meaningful points — not necessarily on every single state change):
import { persist } from "zustand/middleware";

const useAppStore = create(
  persist(
    (set) => ({ draftText: "", setDraftText: (text) => set({ draftText: text }) }),
    { name: "app-draft-storage" }
  )
);

Writing the specific pieces of state a user would actually notice losing — an in-progress form, which screen they were on, a draft — to persistent storage means a cold start after process death can read that state back and reconstruct something close to where the user left off, rather than every restart genuinely starting from zero.

2. Restore navigation state from persisted storage on launch, rather than always defaulting to the initial route

// React Navigation supports restoring its own state tree:
const [isReady, setIsReady] = useState(false);
const [initialState, setInitialState] = useState();

useEffect(() => {
  async function restoreState() {
    const savedStateString = await AsyncStorage.getItem("NAVIGATION_STATE");
    if (savedStateString) setInitialState(JSON.parse(savedStateString));
    setIsReady(true);
  }
  restoreState();
}, []);

Explicitly persisting and restoring the navigation library's own state tree, rather than letting the navigator always initialize to its default first screen, is what actually closes the gap between "the app process died" and "the user experiences this as if nothing happened" for the specific case of where they were in the app.

3. Test deliberately against real process death, not just backgrounding, since the two behave completely differently

# Android developer options -> "Don't keep activities" forces the system
# to destroy every activity the instant it's backgrounded, reliably
# reproducing process-death behavior on demand for testing:
adb shell am kill com.yourapp.package  # also directly kills the process for testing

Ordinary manual testing — backgrounding the app briefly and switching back — essentially never reproduces process death on a developer's own device, which typically has enough free memory to never need to kill anything; deliberately forcing it with the "Don't keep activities" developer option or an explicit adb kill is what actually exercises the restoration logic rather than leaving it untested until a user hits it in the wild.

4. Keep the native splash screen visible slightly longer, bridging the specific gap between activity recreation and the JS bundle being ready

Configure the splash screen to remain visible until the app explicitly signals
readiness (e.g. via react-native-bootsplash's hide() call after the first
meaningful screen has rendered), rather than letting it dismiss automatically
on a fixed timer that doesn't account for cold-start timing variance.

Holding the splash screen until the app itself confirms it has something real to show covers exactly the window where the native shell exists but the JS side hasn't produced a screen yet, turning a visible blank flash into a slightly longer but visually continuous splash screen instead.

Why This Works

Each fix addresses a different part of the same underlying gap between what the user perceives as "resuming" and what's actually a cold start from the system's perspective. Persisting meaningful state gives a cold start something real to restore instead of nothing. Restoring navigation state specifically closes the gap for where the user was in the app, which is often the most noticeable loss. Deliberately testing against forced process death, rather than relying on normal backgrounding, is what actually exercises this restoration path during development instead of leaving it to be discovered in production. And extending the splash screen until the JS side is genuinely ready papers over the brief visual gap inherent to any cold start, regardless of how much state restoration has been implemented.

Conclusion

An app that occasionally shows a blank screen or forgets everything after being backgrounded isn't exhibiting a background-transition bug — it's Android reclaiming memory by killing the entire process, and the subsequent reopen being a genuine cold start that the app's own code has to explicitly account for, because nothing about in-memory JavaScript state survives a killed process on its own. Persist the state a user would actually notice losing, restore navigation state specifically rather than defaulting to the first screen, test deliberately against forced process death rather than ordinary backgrounding, and extend the splash screen until the JS bundle is genuinely ready rather than letting it dismiss on a fixed timer.