skip to content

Delivery by App State

Where a notification lands depends on app state and payload: foreground handlers, background handlers or the tray. Interviewers probe data-only messages and opening from a tap on a killed app.

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

explore

questions

5

With expo-notifications, why does a push not appear while the React Native app is open, and what does setNotificationHandler change?

level: juniorimportance: must knowfreq 52%

answer

  1. foreground: the app decides
  2. no handler means not shown
  3. answer within 3 seconds
  4. banner, list, sound, badge
  5. shouldShowAlert is deprecated

basics

~10 s

In the foreground the app, not the operating system, decides how a notification is presented, and with no handler expo-notifications shows nothing. setNotificationHandler returns a behavior per notification: banner, list, sound and badge.

solid answer

~40 s

When a notification arrives while the app is in the foreground, expo-notifications asks the app what to do with it. The default, when no handler is set or it does not answer in time, is **not to show it**. `Notifications.setNotificationHandler({ handleNotification })` registers the callback: it receives the notification and must resolve a `NotificationBehavior` within **3 seconds**, with `shouldShowBanner`, `shouldShowList`, `shouldPlaySound` and `shouldSetBadge` (the badge flag is iOS-only). `shouldShowAlert` is deprecated in favour of the banner and list flags. In a chat app the handler is where you suppress the banner for the conversation already on screen and show it for any other, while `addNotificationReceivedListener` updates in-app UI such as the unread counter.

code

typescript · 19 lines
typescript
import * as Notifications from 'expo-notifications';

let openConversationId: string | null = null;
export const setOpenConversation = (id: string | null) => {
  openConversationId = id;
};

Notifications.setNotificationHandler({
  handleNotification: async (notification) => {
    const conversationId = notification.request.content.data?.conversationId;
    const alreadyReading = conversationId === openConversationId;
    return {
      shouldShowBanner: !alreadyReading,
      shouldShowList: true,
      shouldPlaySound: !alreadyReading,
      shouldSetBadge: false,
    };
  },
});

go deeper

for a junior

Remember that in the foreground the app decides, that expo-notifications shows nothing without a handler, and the four behavior flags: banner, list, sound, badge.

for a middle

Explain the handler's contract: a behavior within 3 seconds or the notification is dropped, module-scope registration, and how it differs from the received listener.

for a senior

Show production judgment: per-conversation suppression, keeping the decision in memory to meet the deadline, and the Android quirk where disabling sound also removes the heads-up banner.

for a principal

Treat foreground presentation as product policy owned in one place, so every feature that sends notifications gets consistent in-app behavior instead of ad-hoc handlers.

## Who presents a notification, by app state A push that reaches a React Native app is presented by different parties depending on the app's state: - **Background or killed**: for a notification message with a title and body, the **operating system** shows it in the tray or as a banner; no JavaScript needs to run. - **Foreground**: the platform hands the notification to the **app**, which decides whether and how to present it. expo-notifications makes that decision a JavaScript callback. Its documented default is strict: **when no handler is set, or the handler does not respond in time, the notification is not shown**. That is why a team testing with the app open often concludes that "push is broken" when delivery is fine. ## setNotificationHandler `Notifications.setNotificationHandler(handler)` takes an object with: | Member | Required | Purpose | |---|---|---| | `handleNotification(notification)` | Yes | Resolves a `NotificationBehavior` for this notification | | `handleSuccess(id)` | No | Called after the behavior was applied | | `handleError(id, error)` | No | Called when the callback throws or times out | The behavior object has these fields: - `shouldShowBanner` — present it as a banner over the app; - `shouldShowList` — keep it in the notification list; - `shouldPlaySound` — play its sound; on Android, `false` also stops the heads-up drop-down from showing, whatever the priority; - `shouldSetBadge` — update the app icon badge, iOS only; - `priority` — an optional Android priority. `shouldShowAlert` still type-checks but is **deprecated**: expo-notifications logs a warning and asks for `shouldShowBanner` and/or `shouldShowList` instead. On iOS the flags map to the system's presentation options for foreground notifications. Passing `null` clears the handler, which returns the app to the default of showing nothing. ## The 3-second deadline `handleNotification` must resolve **within 3 seconds**, otherwise the notification is discarded and `handleError` receives a timeout error. Consequences: 1. Keep the callback cheap: read in-memory state, do not fetch from the network. 2. Do not wait on a slow storage read to decide; keep the needed state (such as the open conversation id) in memory. 3. Remember that a discarded notification is not retried later. ## Where to call it Call `setNotificationHandler` once, at **module scope** in a file that loads early (the app entry or root layout), not inside a screen. A handler set inside a screen exists only while that screen is mounted, so notifications arriving on other screens fall back to the default and vanish. ## A chat example The classic interview follow-up is the chat case: a message for the conversation the user is currently reading should not pop a banner, while a message for any other conversation should. The handler compares the notification's data with the conversation on screen and answers accordingly. The screen updates its own id in a module-level variable or store when it gains and loses focus. Separately, `Notifications.addNotificationReceivedListener` fires for notifications received while the app is running in the foreground. It does not decide presentation; it is for side effects such as incrementing an unread counter or refreshing the message list. ## Testing the handler Foreground behavior is easy to get wrong because developers usually test with the app open and the handler already set. A short manual matrix catches most defects: 1. App open on the conversation named in the push: no banner, no sound, message appears in the list. 2. App open on another screen: banner and sound. 3. App in the background: the OS shows the notification; the handler is not involved. 4. Handler deliberately slowed past 3 seconds in a debug build: the notification disappears and `handleError` fires. Running the matrix on both platforms matters, because iOS maps the flags to its presentation options while Android maps them to its own display rules, including the sound quirk above. ## Common mistakes - Expecting the OS to show foreground notifications automatically, as it does in the background. - Setting the handler inside one screen's effect. - Doing network work inside `handleNotification` and hitting the 3-second timeout. - Still returning only `shouldShowAlert` and relying on a deprecated field. - Confusing the handler (presentation) with the received listener (side effects).

  • What is the difference between setNotificationHandler and addNotificationReceivedListener in expo-notifications?
    The handler decides presentation: it must return a `NotificationBehavior` within 3 seconds, and without it foreground notifications are not shown. The received listener only observes: it fires for notifications received while the app is in the foreground and is used for side effects such as refreshing the chat list or incrementing an unread counter. It cannot change how the notification is shown.
  • Why can setting shouldPlaySound to false on Android hide the banner as well?
    expo-notifications documents that on Android `shouldPlaySound: false` stops the heads-up drop-down alert from showing regardless of priority, and it overrides channel-specific sounds. So a handler meant to show a silent banner on Android may show no banner at all; the notification still lands in the list if `shouldShowList` is true.

saying these in an interview costs you the question

  • The operating system always shows notifications, even when the app is open
  • Without a handler, expo-notifications shows foreground notifications by default
  • handleNotification can take as long as it needs, for example to fetch data
  • shouldShowAlert is the current way to show a foreground banner
  • addNotificationReceivedListener decides whether the notification is displayed
open as a page

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%

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.

open as a page

With expo-notifications, how do you run JavaScript when a data-only push arrives while the React Native app is killed?

level: middleimportance: should knowfreq 30%

basics

~10 s

Define a task with TaskManager.defineTask and register it with Notifications.registerTaskAsync, both at module scope in an early-loaded file, then send a headless background notification. iOS also needs the remote-notification background mode.

open as a page

With React Native Firebase messaging, why must setBackgroundMessageHandler be registered in index.js, and what may the handler do?

level: middleimportance: should knowfreq 35%

basics

~20 s

It must be set at the top of index.js, outside any component, because background and quit-state messages run without the app's UI. The handler must return a promise, must not update UI, and may fetch data or write storage.

open as a page

In a React Native chat app, why can a notification tap that launches a killed app be missed, and how do expo-notifications and React Native Firebase recover it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The tap that launches a killed app happens before JavaScript has registered any listener, so a listener alone can miss it. Read it at startup: getLastNotificationResponse() or useLastNotificationResponse() in expo-notifications, getInitialNotification() in React Native Firebase.

open as a page