Yashveer Singh
Connect
<- All posts
Cross Platform and Mobile Development12 min read

The Mobile App Lifecycle Hooks Founders Should Know

Mobile app lifecycle states -- foreground, background, and killed -- determine what the app can do, when it can communicate with the user, and how user sessions are defined. Founders who understand lifecycle states make better product decisions about notification timing, session management, data synchronization, and the user experience of returning to the app after a gap. Founders who do not understand lifecycle states are surprised when behaviors they expected (background sync, persistent state, reliable push delivery) do not work the way they assumed.

Written by Yashveer Singh, founder of Yashveer Labs.

What you actually need to know

  • Background execution is unreliable on mobile, especially on Android devices with aggressive battery optimization. Do not design product features that depend on reliable background code execution.
  • Push notifications are the primary mechanism for communicating with users when the app is not in the foreground. They are delivered by the OS, not by the app.
  • Cold launch from a notification (app was killed when the user tapped) is a different code path from foreground from background. Both must be handled.
  • Session definition affects retention and engagement metrics. Define it explicitly before integrating analytics tools.
  • Authentication state, subscription status, and cached data should be refreshed when the app comes to the foreground -- the state may have changed while the app was not running.
App StateUser ExperienceWhat the App Can DoOS Behavior
ForegroundApp is visibleFull capabilitiesNormal execution
BackgroundApp not visible, runningLimited (declared modes only)Aggressive termination on iOS
KilledApp not runningNothingProcess terminated
Cold launchApp was killed, now openingStarting freshCreates new process
Warm launchApp was backgrounded, now foregroundingResuming stateRestores to last visible state

The core argument

Mobile app lifecycle is the part of mobile development that surprises founders most often. The product assumption that users will always experience the app from a fresh foreground state -- like a web page loaded in a browser -- is wrong. Mobile users switch between apps constantly. They receive a notification and open the app from a killed state. They open the app after three days of not using it. They open the app and the operating system has terminated the background process while they were doing something else.

Each of these entry points is a different lifecycle scenario with different engineering requirements. The app that handles only the happy path (user opens app from scratch, uses it, closes it) and does not handle warm launches, cold launches from notifications, or stale background data produces confusing user experiences at the moments that matter most: re-engagement after absence.

Founders who understand lifecycle states can make better product decisions. When the team says "we cannot send a notification when the user has not opened the app in 3 days," a founder who understands that the app is killed and background execution is unavailable understands that push notifications (OS-delivered) are the correct mechanism -- not in-app-triggered code. When the team says "the data is stale when the user comes back," a founder who understands foreground events understands that this is a data refresh timing decision, not an architectural problem.

Foreground and background in practice

Foreground. The app has full access to device capabilities and can execute any code. Network requests, database writes, animations, and user interaction all work normally. This is the state that most feature development targets.

Background. The app continues to run briefly after the user switches to another app or presses the home button. On iOS, the app has a few seconds to complete any in-flight operations before the OS suspends the process. On Android, the behavior is less consistent -- the app may continue running for minutes or may be terminated quickly depending on device battery optimization settings.

Background execution modes that the OS allows (and that must be explicitly declared): audio playback (music, podcast apps), location tracking (navigation, fitness tracking), VoIP (calling apps), background fetch (periodic data refresh, available but unreliable), and background processing (scheduled long-running tasks, iOS 13+). Any code that must run in the background but does not fall into these declared modes should be treated as unreliable.

Killed. The process is not running. The OS has either explicitly terminated it (due to memory pressure or the user force-quitting) or the device was restarted. When the user opens the app from a killed state, it is a cold launch: the app starts from scratch, must re-initialize all state, and must re-establish any connections.

Lifecycle events in React Native

React Native exposes lifecycle events through the AppState API:

```typescript import { AppState, AppStateStatus } from 'react-native'; import { useEffect, useRef } from 'react';

export function useAppLifecycle() { const appState = useRef(AppState.currentState);

useEffect(() => { const subscription = AppState.addEventListener('change', (nextAppState: AppStateStatus) => { if ( appState.current.match(/inactive|background/) && nextAppState === 'active' ) { // App has come to the foreground handleForeground(); }

if (nextAppState === 'background') { // App is going to the background handleBackground(); }

appState.current = nextAppState; });

return () => subscription.remove(); }, []); }

function handleForeground() { // Refresh stale data, re-check authentication, sync pending changes }

function handleBackground() { // Cancel unnecessary network requests, save unsaved state } ```

The handleForeground function is where data refresh logic lives. For most apps, this means: check if the auth token has expired (re-authenticate if so), check if the user's subscription status may have changed (refresh from the server if more than N minutes have elapsed), and refresh any content that may have changed since the app was last in the foreground.

Push notification lifecycle handling

Push notifications require handling two lifecycle scenarios: the notification was tapped when the app was killed (cold launch), and the notification was tapped when the app was in the background (warm launch).

In React Native with @react-native-firebase/messaging:

```typescript // Handle notification that launched the app (cold launch) useEffect(() => { messaging().getInitialNotification().then(notification => { if (notification) { navigateToNotificationDestination(notification.data); } });

// Handle notification tapped while app was in background const unsubscribe = messaging().onNotificationOpenedApp(notification => { navigateToNotificationDestination(notification.data); });

return unsubscribe; }, []);

function navigateToNotificationDestination(data: NotificationData) { switch (data.type) { case 'new_message': navigation.navigate('Chat', { conversationId: data.conversationId }); break; case 'payment_failed': navigation.navigate('Billing'); break; default: navigation.navigate('Home'); } } ```

Both cases use the same navigateToNotificationDestination function, which reads the notification payload and navigates to the correct screen. The cold launch case uses getInitialNotification() (checked once at app startup). The background case uses the onNotificationOpenedApp listener.

Data freshness management on foreground

The data that is cached in the app when the user backgrounds it may be stale when they return. How stale depends on how long the app was backgrounded. For short gaps (seconds to minutes), cached data is typically fresh enough. For longer gaps (hours to days), the data may have changed significantly.

The foreground refresh strategy:

Always refresh: authentication state, subscription/access status, user account settings. These are security-critical; stale state could allow access that should no longer be permitted.

Refresh if stale: content feeds, notification badges, user-specific data that changes with some frequency. Define the staleness threshold (5 minutes, 30 minutes, 2 hours) based on how frequently the data changes and how bad the user experience of stale data is.

Do not refresh: static content, reference data that rarely changes. These can be refreshed on a longer schedule or on explicit user action.

Common mistakes founders make with mobile lifecycle

  1. Expecting background data sync to work reliably. Features that require the app to fetch and process data in the background (sync contacts, pre-load content, update cache) are unreliable on iOS and inconsistent on Android. Design these features to work when the app is foregrounded instead, with graceful degradation when they cannot complete in the background.
  2. Not handling the cold launch from notification case. The notification that launches the app from a killed state is a different code path from the notification that brings a backgrounded app to the foreground. Both must be tested explicitly.
  3. Not refreshing authentication state on foreground. An auth token that expired while the app was backgrounded produces a confusing experience: the user opens the app and gets a 401 error on the first API call. Check auth state on every foreground event.
  4. Defining sessions for mobile the same way as for web. A web session is time-based and tied to the browser. A mobile session is gap-based and tied to foreground periods. Using web session logic for mobile analytics produces metrics that do not reflect actual mobile usage patterns.
  5. Not testing background-to-foreground transitions with stale data. The test that runs the app continuously without backgrounding does not catch the foreground refresh failure that only occurs when the app has been in the background for more than the staleness threshold.

Where to start: a 3-step lifecycle implementation

Step 1: Implement the foreground event handler and add auth state refresh. Every foreground event should trigger a check of the authentication state. If the token has expired, re-authenticate silently (using refresh tokens) or redirect to the login screen. This is the minimum viable lifecycle handling for any app with authentication.

Step 2: Implement both push notification cases (cold launch and background-to-foreground) with navigation. Define the notification payload format, implement the navigation logic, and test both cases explicitly: send a push notification with the app killed (cold launch path) and with the app backgrounded (background-to-foreground path).

Step 3: Define the session boundary and configure it consistently across analytics and backend. The session gap (how long in the background before a new session starts) should be the same in the analytics tool configuration, the backend session tracking logic, and any local state management that uses sessions.

The Lifecycle That Shapes the User Experience

Yashveer Singh. Founder of Yashveer Labs. The Prominence Football Academy app had a lifecycle bug in its first TestFlight: the push notification for a match schedule update navigated correctly when the app was in the background, but opened to the home screen when the app was killed. The cold launch case was not handled. It was found in the first week of testing and fixed before public launch -- but the scenario was only discovered because the testing protocol explicitly included cold launch testing. Most mobile bugs live in the transition cases: cold launch, background-to-foreground, stale auth, interrupted network requests. The happy path is easy to test. The lifecycle edge cases are the ones that reach users if the testing protocol does not explicitly include them.

Related reading

FAQ

Frequently asked

Author

About the author and why it matters

Yashveer Singh wrote this. I run Yashveer Labs out of New Delhi. The work I take on tends to come from founders who have been burned by an agency, a freelancer, or their own ambition. I do not promise miracles. I promise that the system will be online, the code will be readable, and the next engineer who touches it will not curse me. That is rarer than it should be.

Related reading