skip to content

App State & Data

Where client state lives in a React Native app, how it survives restarts, and what happens as the OS moves the app to the background. Interviewers push on offline writes and background work.

part ofReact Nativeoverview, primer and where to startread it →
on this pageshow

explore

questions

19

In React Native, what do the AppState values active, background and inactive mean, and how do you subscribe to changes?

level: juniorimportance: must knowfreq 62%

answer

  1. three values, one iOS-only
  2. inactive: iOS transitions and interruptions
  3. currentState for a synchronous read
  4. addEventListener('change') returns a subscription
  5. subscription.remove() in the effect cleanup

basics

~10 s

React Native's AppState reports active (foreground), background, or, on iOS only, inactive during transitions. Read AppState.currentState, subscribe with AppState.addEventListener('change', handler), and call remove() on the returned subscription.

solid answer

~40 s

`active` means the app is in the foreground; `background` means the user is in another app or on the home screen, or, on Android, another `Activity` covers yours; `inactive` is iOS-only and marks transitions and interruptions such as the app switcher, Notification Center or an incoming call. `AppState.currentState` gives the latest value synchronously, and `AppState.addEventListener('change', handler)` calls the handler with each new value. The call returns an `EventSubscription`, and you call `subscription.remove()` in the `useEffect` cleanup, because current React Native has no `AppState.removeEventListener`. To detect a return to the foreground, keep the previous value in a ref and act when it moves from `inactive` or `background` to `active`.

code

tsx · 20 lines
tsx
import { useEffect, useRef, useState } from 'react';
import { AppState, type AppStateStatus } from 'react-native';

export function useAppState(onForeground?: () => void): AppStateStatus {
  const previous = useRef<AppStateStatus>(AppState.currentState);
  const [state, setState] = useState<AppStateStatus>(AppState.currentState);

  useEffect(() => {
    const subscription = AppState.addEventListener('change', next => {
      if (/inactive|background/.test(previous.current) && next === 'active') {
        onForeground?.();
      }
      previous.current = next;
      setState(next);
    });
    return () => subscription.remove();
  }, [onForeground]);

  return state;
}

go deeper

for a junior

Name the three values, say that inactive is iOS-only, and show the useEffect that subscribes with addEventListener and calls remove() in its cleanup.

for a middle

Explain how to detect a return to the foreground with a previous-value ref, and why the iOS sequences differ from Android's: inactive then background on leaving, no inactive step on Android.

for a senior

Show that you design lifecycle handlers per platform: Android reports background when another Activity covers the app, iOS reports inactive for brief interruptions, and handlers must tolerate both.

for a principal

Argue for one shared app-lifecycle hook or service instead of scattered listeners, so pause and resume rules are decided once and reviewed per platform.

## What AppState reports `AppState` is a core React Native module (imported from `react-native`) that tells JavaScript whether the app is in the foreground and notifies it when that changes. It is backed by a native module on each platform: on iOS it listens to the `UIApplication` lifecycle notifications, and on Android it listens to the host Activity's resume and pause callbacks. Its value is a short string, and three of them matter in everyday code: | Value | Platforms | When you see it | |---|---|---| | `active` | iOS and Android | The app is in the foreground and the user can interact with it | | `background` | iOS and Android | The user is in another app or on the home screen; on Android also when another `Activity` covers yours, including system activities such as autofill credential pickers | | `inactive` | iOS only | A transition or interruption: moving between foreground and background, the multitasking view, Notification Center, an incoming call | The TypeScript type `AppStateStatus` also lists `unknown` and `extension`, both iOS-only edge cases (a value before the state is known, and code running inside an app extension). Android's native module only ever produces `active` or `background`, so code that waits for `inactive` on Android waits forever. ## Reading the current value `AppState.currentState` is a synchronous read of the latest known value. React Native keeps it current by registering its own internal listener, so you can read it at any time, for example to initialise a `useRef` or `useState`. Under the New Architecture, the only architecture since React Native 0.82, the initial value comes from the native module's constants when `AppState` is first loaded, so it holds the launch state. The documentation notes that under the old legacy architecture it could be `null` until the native side answered asynchronously, which is why older code guards against `null`. ## Subscribing and unsubscribing `AppState.addEventListener(type, handler)` registers a handler and **returns an `EventSubscription`**. The events are: - `change`: the handler receives the new `AppStateStatus` string; - `memoryWarning`: iOS only, fired when the system issues a memory warning; - `focus` and `blur`: Android only, fired when the app's window gains or loses focus (for example when the notification drawer is pulled down) while the state stays `active`. To stop listening you call `remove()` on the returned subscription. Current React Native has no `AppState.removeEventListener`; `AppState` also stopped inheriting from `NativeEventEmitter` years ago, so `addListener` and `removeAllListeners` are not on it either. In a function component the pattern is a `useEffect` that subscribes and returns a cleanup: ```tsx useEffect(() => { const subscription = AppState.addEventListener('change', next => { console.log('AppState is now', next); }); return () => subscription.remove(); }, []); ``` Forgetting the cleanup leaves the handler registered after the component unmounts, so it keeps running against stale state. ## Detecting a return to the foreground A common task is "do something when the user comes back": refresh a timestamp, re-check a permission, resume a paused feature. The `change` event gives only the new value, so you keep the previous one yourself, usually in a `useRef` initialised from `AppState.currentState`, and act when the previous value was `inactive` or `background` and the new one is `active`. Two platform details shape this: 1. On iOS, leaving the app normally produces `inactive` and then `background`; coming back produces `active` directly. 2. On iOS, a brief interruption such as Notification Center produces `inactive` and then `active` again without ever reaching `background`. 3. On Android, leaving produces `background` and coming back produces `active`; there is no `inactive` step. Matching the previous value against both `inactive` and `background` handles all three sequences with one check. ## Common mistakes - Treating `inactive` as cross-platform and writing Android logic that depends on it. - Calling a `removeEventListener` method that current React Native does not have. - Reading `currentState` once on mount and never subscribing, so the value goes stale. - Assuming `active` says which screen is focused; `AppState` knows about the whole app, not about navigation. - Re-creating the subscription on every render by passing an unstable callback in the effect's dependency list. `AppState` observes the lifecycle; it does not change it. Registering a listener neither keeps the app running in the background nor delays suspension.

  • Besides 'change', which events can a React Native app subscribe to on AppState, and on which platforms?
    `memoryWarning` fires on iOS when the system issues a memory warning, a cue to drop caches. `focus` and `blur` are Android-only and follow the window's focus: pulling down the notification drawer fires `blur` while the state stays `active`, and closing it fires `focus`. All of them use the same `addEventListener` call and return a subscription you `remove()`.
  • Why does React Native code often guard AppState.currentState against null, and is that still needed in 0.87?
    Under the legacy architecture the initial value was fetched asynchronously from native code, so `currentState` could be `null` right after launch. In the New Architecture, the only architecture since 0.82, it is initialised from the native module's constants when `AppState` loads. A guard does no harm, but it is legacy caution, not a requirement.

saying these in an interview costs you the question

  • AppState reports inactive on Android as well as on iOS.
  • You unsubscribe with AppState.removeEventListener('change', handler).
  • AppState.currentState is always null until the first change event fires.
  • AppState tells you which navigation screen is currently focused.
  • Adding an AppState listener keeps the app running in the background.
open as a page

In an offline-capable React Native app, why must pending writes go into a persisted outbox rather than memory, and when is it replayed?

level: juniorimportance: must knowfreq 40%

basics

~20 s

Pending writes held in memory vanish when the OS kills the app or it crashes. A persisted outbox keeps each write on disk until the server confirms it, replaying on reconnect, at launch and on return to the foreground.

open as a page

With a React Native store persisted to AsyncStorage, why does a workout app first flash an empty log, and how do you gate on hydration?

level: middleimportance: must knowfreq 55%

basics

~20 s

AsyncStorage reads are asynchronous, so the store starts with its initial state and the saved log arrives a moment later. Gate on hydration: redux-persist's PersistGate renders its loading prop until rehydration completes, and Zustand exposes persist.hasHydrated and onFinishHydration.

open as a page

In a React Native sneaker store, why can a cart kept in one React Context make low-end Android phones stutter on every quantity change?

level: middleimportance: must knowfreq 58%

basics

~20 s

Every component reading the cart Context re-renders when its value changes, including those on mounted but hidden screens. That work runs on React Native's single JavaScript thread, so a slow phone misses frames and taps lag.

open as a page

With expo-task-manager in a React Native app, why must TaskManager.defineTask be called at module top level rather than inside a component?

level: juniorimportance: should knowfreq 22%

basics

~20 s

When the OS wakes an Expo app for a task, React Native evaluates the JavaScript bundle but mounts no views, so a defineTask inside a component never runs. expo-task-manager then finds no task, logs a warning and unregisters it.

open as a page

In a React Native workout app, what belongs in a persisted store, and how do partialize or a redux-persist whitelist keep the rest out?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Persist only what the user would lose work or context without, such as the unfinished workout log and preferences, not transient flags, refetchable server data or secrets. Zustand's partialize and redux-persist's whitelist or blacklist choose the subset.

open as a page

In React Native on Android, how do AppRegistry.registerHeadlessTask and HeadlessJsTaskService work together to run JavaScript in the background?

level: middleimportance: should knowfreq 32%

basics

~10 s

Headless JS is Android-only: AppRegistry.registerHeadlessTask registers an async function under a key, and a HeadlessJsTaskService subclass returns a HeadlessJsTaskConfig naming that key, so native triggers can run it without UI until its promise resolves.

open as a page

In an Expo app, why does a task registered with BackgroundTask.registerTaskAsync and minimumInterval: 15 not run every 15 minutes?

level: middleimportance: should knowfreq 38%

basics

~20 s

expo-background-task hands the work to WorkManager on Android and BGTaskScheduler on iOS, which run it when battery, network and usage patterns suit them; minimumInterval is only a floor in minutes, so runs are best effort.

open as a page

In React Native, what happens to the JavaScript runtime, its timers and your in-memory state after the app moves to the background?

level: middleimportance: should knowfreq 45%

basics

~20 s

The JavaScript runtime stays in memory but stops getting time: React Native pauses its timers, iOS soon suspends the process, and either OS may later kill it without any JavaScript event. Save state when AppState reports background.

open as a page

With TanStack Query in a React Native app, how do paused mutations work offline, and what must you wire up so they resume after reconnecting or restarting?

level: middleimportance: should knowfreq 34%

basics

~20 s

With networkMode 'online', a TanStack mutation made offline pauses and resumes when onlineManager reports online. React Native has no browser online events, so wire onlineManager to NetInfo; for restarts, persist mutations and register mutationFn with setMutationDefaults.

open as a page

In React Native, what changes for a persisted Zustand or redux-persist store when its storage adapter moves from AsyncStorage to MMKV?

level: middleimportance: should knowfreq 38%

basics

~20 s

MMKV reads and writes synchronously, so a Zustand persist store hydrates while it is created and needs no loading gate, while redux-persist wraps it in promises and stays asynchronous. Serialization still runs on the JavaScript thread, and existing AsyncStorage data must be migrated.

open as a page

In a React Native app, how does code outside components, such as a Headless JS task or notification handler, read or update the cart store?

level: middleimportance: should knowfreq 35%

basics

~10 s

Context is readable only by rendered components, so code outside the tree needs an importable external store: Redux's getState and dispatch, a Zustand store's getState and setState, or a Jotai store's get and set.

open as a page

A React Native running app must upload the GPS track while backgrounded; why is a periodic background task not enough, and how would you design the sync?

level: seniorimportance: should knowfreq 26%

basics

~20 s

A periodic expo-background-task runs when the OS chooses, possibly hours later. Background location keeps the app running, so sync inside the TaskManager location task: persist each batch, then upload pending points, with a periodic task as backstop.

open as a page

A React Native video call pauses the camera whenever AppState leaves 'active'; why does video drop out on iOS, and which transitions should pause and resume it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

On iOS, AppState reports inactive for brief interruptions such as Notification Center, the app switcher or a permission prompt, so a check for anything but active tears video down needlessly. Pause on background, resume on active, and guard both.

open as a page

A React Native warehouse app replays queued scans after reconnecting, and the server records some scans twice; why, and how do idempotency keys fix it?

level: seniorimportance: should knowfreq 33%

basics

~20 s

Scans repeat when the server commits but the response is lost, the app dies before dequeuing, or replays overlap. An idempotency key created when the scan is queued lets the server recognise repeats and return the first result.

open as a page

In an offline-first React Native app using TanStack Query optimistic updates, why can a scan vanish from the screen and never sync, and when should rollback happen?

level: seniorimportance: should knowfreq 28%

basics

~20 s

If TanStack Query thinks it is online, an offline scan fails with a network error; with the default retry of 0, onError rolls it back and ends it. Pause offline, retry transient errors, roll back only on a definitive rejection.

open as a page

A React Native release renames a field in the persisted workout store; how do version and migrate protect returning users' unfinished workouts, and what makes migrations fail silently?

level: seniorimportance: should knowfreq 35%

basics

~20 s

The saved entry carries a version; when it differs from the code's, the store runs your migrate step to convert the old shape. Migrations fail silently on a missed bump, a throwing step or a timeout: the store starts empty and its next write erases the workout.

open as a page

In a React Native product FlatList where every row reads the whole cart from a store, why does adding one sneaker re-render every rendered row, and how do you narrow it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

A store re-renders a subscriber when its selected result changes, and the whole cart is a new object after every update. Give each row a selector for a primitive about its own sneaker, such as its quantity.

open as a page

Your team is starting a React Native sneaker store for users on low-end Android phones; how would you choose between Context, Redux Toolkit, Zustand and Jotai for client state?

level: principalimportance: should knowfreq 38%

basics

~20 s

Move server data to a query cache first, then size what remains. Context suits rare changes, Zustand or Jotai suit small fast-changing state with narrow subscriptions, and Redux Toolkit suits large teams wanting enforced structure.

open as a page