skip to content

In a React Native chat app, how does push delivery differ between foreground, background and killed states for notification versus data-only messages?

level: middleimportance: must knowfreq 60%

answer

  1. who presents it: app or OS
  2. foreground always goes to JS
  3. OS shows notification messages when not in front
  4. data-only shows nothing, runs code
  5. high priority / content-available

basics

~20 s

Notification messages are shown by the operating system when the app is backgrounded or killed, while in the foreground the app's handler decides. Data-only messages are not displayed by the OS; they exist to run app JavaScript, and the OS may not deliver them.

solid answer

~50 s

There are two axes. For a **notification message** (title and body): in the foreground JavaScript receives it and decides presentation (expo-notifications' handler, or React Native Firebase's `onMessage`, where nothing is shown unless the app shows it); in the background or killed state the OS displays it itself. For a **data-only message**: the OS displays nothing in any state; it exists to run code, through expo-notifications' `registerTaskAsync` task or React Native Firebase's `setBackgroundMessageHandler`. Both platforms treat data-only messages as low priority, so they wake a backgrounded or killed app only when sent with high priority on Android and `content-available` on iOS, and even then delivery is not guaranteed. A user who force-stops an Android app receives nothing until they reopen it. For a chat app that means: show chat messages as notification messages, and use data-only pushes only for background sync.

code

tsx · 12 lines
tsx
import { useEffect } from 'react';
import { getMessaging, onMessage } from '@react-native-firebase/messaging';

export function useForegroundChatMessages(onChatMessage: (conversationId: string) => void) {
  useEffect(() => {
    const unsubscribe = onMessage(getMessaging(), async (remoteMessage) => {
      const conversationId = remoteMessage.data?.conversationId;
      if (typeof conversationId === 'string') onChatMessage(conversationId);
    });
    return unsubscribe;
  }, [onChatMessage]);
}

go deeper

for a junior

Remember that the OS shows notification messages when the app is not on screen, and that in the foreground your code must decide what to show.

for a middle

Explain the full matrix for both libraries, including which handler runs in each state and why data-only messages need high priority or content-available.

for a senior

Choose message types for a real product: user-facing chat as notification messages, data-only only for best-effort sync, and a design that survives background messages never running.

for a principal

Frame the tradeoff between reliable OS-rendered notifications and flexible app-rendered ones, and set the rule every team follows when adding a new push type.

## Two axes: app state and message type Where a push lands in a React Native app depends on **the app's state** when it arrives and **what the message contains**. App states, as the Expo and React Native Firebase docs define them: - **Foreground** — the app is on screen. - **Background** — the app is running but not visible. - **Killed** (terminated, or "quit") — the app is not running, typically after being swiped away. Message types: - **Notification message** — carries presentational fields such as a title and body, and may also carry data. - **Data-only message** — carries only JSON data and no presentation. Expo calls this a **headless background notification**; on iOS the closest equivalent is a background notification with `content-available`. ## The matrix With expo-notifications: | Message | Foreground | Background | Killed | |---|---|---|---| | Notification message | Handler decides; received listener and task run | OS shows it | OS shows it | | Data-only (headless) | Received listener and task run | Task runs | Task runs | With React Native Firebase messaging: | Message | Foreground | Background | Quit | |---|---|---|---| | Notification (+ data) | `onMessage`, nothing displayed | OS displays; `setBackgroundMessageHandler` runs | OS displays; `setBackgroundMessageHandler` runs | | Data-only | `onMessage` | `setBackgroundMessageHandler`, only if high priority / content-available | same as background | Two consequences stand out. First, **in the foreground nothing is shown for you**: React Native Firebase never displays a notification from `onMessage`, and expo-notifications shows nothing unless a handler says so. Second, **the OS never displays a data-only message**; if the user should see it, app code must present a local notification from the background handler. (expo-notifications makes one documented exception: on Android it presents a headless message itself when the data contains `title` or `message`.) ## Why data-only delivery is fragile Data-only messages ask the OS to run your code, which costs battery, so both platforms restrict them: 1. They are treated as **low priority** and are ignored in the background and killed states unless sent with **high priority** on Android and **`content-available`** on iOS. 2. Even then the OS may **not deliver** them: Android's Doze mode can defer them, and Apple recommends sending no more than two or three background notifications per hour. 3. On iOS, headless handling requires the `remote-notification` background mode. 4. On Android, a user who **force-stops** the app from Settings receives nothing until they open it again. The Expo documentation's rule of thumb follows: prefer a regular notification message unless you need to run JavaScript in the background. ## Applying it to a chat app - **New message for the user**: send a notification message. It appears reliably in the background and killed states, and the foreground handler can suppress it for the open conversation. - **Keeping the cache fresh**: an extra data-only message can trigger a background sync so the conversation is already loaded when the user opens it, but the design must survive that message never running. - **Read receipts or typing indicators**: these only matter while the app is open, so they belong to the foreground connection, not to push. ## Testing the matrix Each cell of the matrix is a separate test case, and the killed column is the one most often skipped: - swipe the app away before sending, rather than just pressing Home; - send one notification message and one data-only message for each state; - on Android, repeat once with the device idle long enough to enter Doze, to see a data-only message deferred; - on iOS, confirm the background mode is present before concluding a data-only message "does not work". ## What runs where - expo-notifications: `setNotificationHandler` and `addNotificationReceivedListener` in the foreground, the `registerTaskAsync` task for headless messages in any state. - React Native Firebase: `onMessage` in the foreground, `setBackgroundMessageHandler` in the background and quit states, run on Android as a Headless JS task. ## Common mistakes - Sending chat messages as data-only and presenting them locally, then losing them whenever the OS does not wake the app. - Expecting a notification to appear automatically in the foreground. - Assuming `onMessage` fires when the app is in the background. - Treating a killed app as unable to receive any push.

  • If a chat message must reach the user even when the app is killed, why prefer a notification message over data-only plus a local notification?
    The OS displays a notification message itself in the background and killed states, with no JavaScript involved. The data-only route depends on the OS waking the app, which requires high priority or content-available and is still not guaranteed under Doze or Apple's background budget. When the app is not woken, the user never sees the message.
  • What happens to push delivery on Android after the user force-stops the app from Settings?
    Both the Expo and React Native Firebase docs note that a force-stopped Android app receives no messages until the user opens it again. Swiping the app away is different: that is the ordinary killed state, in which notification messages are still displayed by the OS.

A notification message is a letter the postman leaves in the mailbox: it is there whether or not anyone is home. A data-only message is a note that only matters if someone is home to act on it, and the building manager may decide not to wake them.

saying these in an interview costs you the question

  • A killed React Native app cannot receive any push at all
  • React Native Firebase shows a notification automatically when onMessage fires
  • Data-only messages are shown to the user like notification messages
  • Data-only messages always wake the app, whatever their priority
  • onMessage also fires when the app is in the background