skip to content

Push Notification Handling

Receiving notifications in React Native means a device token from APNs or FCM, a permission prompt, handlers per app state and Android channels. Interviewers probe the killed-app tap.

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

explore

questions

21

With expo-notifications, how do you schedule a daily local medication reminder in a React Native app, and which trigger types exist?

level: juniorimportance: must knowfreq 50%

answer

  1. local: scheduled on the device, no server
  2. scheduleNotificationAsync({ content, trigger })
  3. trigger type from SchedulableTriggerInputTypes
  4. returns an identifier to cancel
  5. weekday 1-7, Sunday is 1

basics

~10 s

Call Notifications.scheduleNotificationAsync with content and a trigger such as { type: SchedulableTriggerInputTypes.DAILY, hour, minute }; it resolves an identifier for cancelling. Other triggers: DATE, TIME_INTERVAL, WEEKLY, MONTHLY, YEARLY and iOS-only CALENDAR.

solid answer

~40 s

A local notification is scheduled on the device itself, so no server or push token is involved. `Notifications.scheduleNotificationAsync({ content, trigger })` takes the content (`title`, `body`, `data`) and a trigger object whose `type` comes from `Notifications.SchedulableTriggerInputTypes`: `DAILY` (`hour`, `minute`), `WEEKLY` (`weekday`, `hour`, `minute`), `MONTHLY`, `YEARLY`, `DATE` (one-off), `TIME_INTERVAL` (`seconds`, optional `repeats`) and `CALENDAR`, which is iOS-only. A `null` trigger delivers immediately. On Android the trigger also carries the `channelId`. The call resolves a string identifier: store it per medication so `cancelScheduledNotificationAsync(id)` can remove the reminder when the dose changes. Date components are validated: `weekday` runs 1-7 with 1 meaning Sunday, `month` 0-11, and out-of-range values throw.

code

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

export async function scheduleWeeklyDose(medId: string, jsDate: Date): Promise<string> {
  return Notifications.scheduleNotificationAsync({
    content: { title: 'Weekly injection', body: 'Time for your dose', data: { medId } },
    trigger: {
      type: Notifications.SchedulableTriggerInputTypes.WEEKLY,
      weekday: jsDate.getDay() + 1, // getDay() is 0-6 from Sunday; weekday is 1-7 from Sunday
      hour: jsDate.getHours(),
      minute: jsDate.getMinutes(),
      channelId: 'meds',
    },
  });
}

go deeper

for a junior

Recall scheduleNotificationAsync with content and a trigger, the DAILY trigger's hour and minute, and that the call resolves an identifier you keep for cancelling.

for a middle

Explain the trigger types and their platform limits, the 1-7 weekday and 0-11 month ranges, and why a repeating interval is not a clock-time schedule.

for a senior

Treat the device schedule as state to reconcile: store identifiers per medication, cancel on change, avoid duplicates on launch and verify with getAllScheduledNotificationsAsync.

for a principal

Decide which reminders the device owns and which the server owns, and how the two stay consistent when a regimen changes on another device.

## Local versus push A **local notification** is created and scheduled by the app on the device. Nothing is sent from a server, no push token is needed, and the operating system delivers it at the scheduled moment even if the app is not running. That makes it the natural fit for **medication reminders**, whose times are known on the device. Two things still apply as for any notification: the app needs notification permission, and on Android the notification belongs to a channel. ## The call ```ts const id = await Notifications.scheduleNotificationAsync({ content: { title: 'Metformin 500 mg', body: 'Time for your morning dose', data: { medId: 'met' } }, trigger: { type: Notifications.SchedulableTriggerInputTypes.DAILY, hour: 8, minute: 30, channelId: 'meds' }, }); ``` - `content` holds what is shown (`title`, `subtitle`, `body`) and hidden `data` for the app. - `trigger` says when; every trigger object carries a `type`. - The promise resolves an **identifier**, used later to cancel or to recognise the notification. ## The trigger types | Type | Fields | Repeats | Platforms | |---|---|---|---| | `DATE` | `date` (a `Date` or timestamp) | Never | Both | | `TIME_INTERVAL` | `seconds`, `repeats?` | If `repeats: true` | Both; on iOS a repeating interval must be at least 60 s | | `DAILY` | `hour`, `minute` | Every day | Both | | `WEEKLY` | `weekday`, `hour`, `minute` | Every week | Both | | `MONTHLY` | `day`, `hour`, `minute` | Every month | Both | | `YEARLY` | `month`, `day`, `hour`, `minute` | Every year | Both | | `CALENDAR` | date components, `repeats?` | If `repeats: true` | iOS only | A `null` trigger means "deliver now", and passing a bare `Date` or number is treated as a `DATE` trigger. Every trigger accepts an optional `channelId` for Android. ## Date components are validated, and not all are zero-based expo-notifications checks the components before scheduling and throws a `RangeError` when one is out of range: - `hour` 0-23 and `minute` 0-59; - `weekday` **1-7, where 1 is Sunday**, which differs from JavaScript's `Date.getDay()` (0 is Sunday); - `month` **0-11**, matching JavaScript's `Date`; - `day` from 1 to the number of days in that month. Passing `new Date().getDay()` straight into a weekly trigger therefore schedules every reminder one day early, and throws on Sundays. ## Managing the schedule 1. **Store the identifier** with the medication, so a changed dose time can cancel exactly its own reminders. 2. **Cancel** with `Notifications.cancelScheduledNotificationAsync(id)`, or everything with `cancelAllScheduledNotificationsAsync()`. 3. **Inspect** with `getAllScheduledNotificationsAsync()`, and preview a trigger with `getNextTriggerDateAsync(trigger)`. 4. **Reconcile on launch**: compare the stored plan with what is scheduled and fix drift, rather than scheduling duplicates every time the app opens. ## Designing a medication schedule A regimen rarely maps to a single trigger. Typical translations: - **Twice daily** (08:00 and 20:00): two `DAILY` triggers, one identifier each. - **Once a week** on Mondays: one `WEEKLY` trigger with `weekday: 2`. - **A 10-day course**: `DAILY` triggers have no end date, so either schedule ten `DATE` triggers or keep a `DAILY` trigger and cancel it when the course ends. - **Every 8 hours from the first dose**: a repeating `TIME_INTERVAL` of 28,800 seconds, which is anchored to when it was scheduled, matching the regimen. Store the regimen as data and derive the scheduled notifications from it, so a change to the regimen always produces the same set of triggers. ## Presentation when the app is open Scheduling does not guarantee a banner. If the reminder fires while the app is in the foreground, expo-notifications shows it only if a notification handler says so; that is the same foreground rule as for push. ## Local or push for reminders Interviewers often ask why not send each reminder from a server. For fixed-time reminders, local scheduling wins: it works offline, needs no token or backend job, and fires even if the network is down at dose time. A server is still useful to back up the regimen and to change it from another device, after which the app reschedules locally. ## Common mistakes - Using a repeating 24-hour `TIME_INTERVAL` for "every day at 08:30": it repeats from the moment of scheduling, not at 08:30. - Passing `Date.getDay()` as `weekday`. - Using `CALENDAR` and expecting it to work on Android. - Scheduling on every launch without cancelling, which piles up duplicate reminders.

  • Why is a repeating TIME_INTERVAL of 86400 seconds a poor way to remind someone every day at 08:30?
    A repeating interval counts from the moment it was scheduled, so a reminder set at 14:10 fires at 14:10 every day, not at 08:30. The `DAILY` trigger matches the `hour` and `minute` components instead, which is what a medication time means.
  • How should the app update a reminder when the user moves a dose from 08:30 to 09:00?
    Cancel the old reminder with `cancelScheduledNotificationAsync` using the identifier stored for that medication, schedule the new `DAILY` trigger, and store the new identifier. Scheduling without cancelling leaves both reminders active.

saying these in an interview costs you the question

  • Local notifications need a push token and a server to fire
  • The CALENDAR trigger works the same on Android and iOS
  • WEEKLY weekday values use JavaScript's getDay() numbering
  • A repeating 24-hour TIME_INTERVAL fires at the same clock time each day
  • Scheduling the same reminder on every launch is harmless
open as a page

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%

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.

open as a page

In a React Native app, how does asking for push notification permission differ between iOS and Android 13 or later?

level: juniorimportance: must knowfreq 62%

basics

~20 s

iOS requires every app to request notification authorization through a one-time system alert. Android showed notifications by default until Android 13 (API 33) added the POST_NOTIFICATIONS runtime permission, which must be declared in the manifest and requested.

open as a page

In a React Native app, what is a push device token, and which provider issues it on iOS and on Android?

level: juniorimportance: must knowfreq 60%

basics

~20 s

A device token is an opaque address the push provider issues for one app installation. On iOS it comes from APNs, on Android from FCM; the app sends it to its backend, which uses it to target that install.

open as a page

In a React Native app on Android, why does changing an existing notification channel's importance or sound have no effect?

level: middleimportance: must knowfreq 42%

basics

~20 s

Once an Android channel exists, its behavior (importance, sound, vibration) is under the user's control; recreating it with the same id updates details such as name and description and can at most lower the importance. To change behavior, use a new channel id and delete the old one.

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

In a React Native habit-tracker app, when should you trigger the notification permission prompt for reminders, and why not at first launch?

level: middleimportance: must knowfreq 55%

basics

~20 s

Trigger it when the user asks for something that needs it, such as setting their first habit reminder, after a short in-app explanation. A cold prompt at launch gets refused, and iOS shows its alert only once.

open as a page

With expo-notifications, how do you add Taken and Snooze buttons to a medication reminder, and how does the app learn which was pressed?

level: middleimportance: should knowfreq 30%

basics

~10 s

Register a category with Notifications.setNotificationCategoryAsync(identifier, actions) at startup, set content.categoryIdentifier on each reminder, then read response.actionIdentifier from the notification response to see which button was pressed.

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

With expo-notifications on Android 13 or later, why must a notification channel exist before you request notification permission?

level: middleimportance: should knowfreq 30%

basics

~10 s

Expo's documentation states that on Android 13 the notification permission prompt will not appear until at least one channel exists. So call setNotificationChannelAsync before requestPermissionsAsync and before fetching a push token.

open as a page

In a React Native app, how do you handle a user who has denied notification permission, including sending them to the system Settings?

level: middleimportance: should knowfreq 45%

basics

~20 s

Check whether the system can still ask (canAskAgain in expo-notifications, never_ask_again from PermissionsAndroid). If not, explain the benefit and open the app's settings with Linking.openSettings(), then re-check the permission when the app returns to the foreground.

open as a page

Why might a React Native app get no usable push token in Expo Go, an iOS Simulator or an Android emulator?

level: middleimportance: should knowfreq 40%

basics

~20 s

Expo Go has no remote push since SDK 53, Android emulators need a Google Play image for FCM, and React Native Firebase v26 skips APNs registration on Apple-silicon Simulators. Test on a development build, ideally on a physical device.

open as a page

In an Expo app, when do you use Notifications.getExpoPushTokenAsync() versus getDevicePushTokenAsync(), and what does each return?

level: middleimportance: should knowfreq 45%

basics

~10 s

getExpoPushTokenAsync() returns an ExponentPushToken for sending through Expo's push service, which relays to APNs or FCM; getDevicePushTokenAsync() returns the native APNs or FCM token for servers that talk to those providers directly.

open as a page

A medication reminder scheduled with expo-notifications fires minutes late on some Android 12+ devices; why, and what do you change?

level: seniorimportance: should knowfreq 25%

basics

~20 s

On Android 12+ expo-notifications uses an exact alarm only when the app is allowed to schedule exact alarms; otherwise it falls back to an inexact alarm the system may delay. Declare SCHEDULE_EXACT_ALARM, and handle devices where it is still unavailable.

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

In a bare React Native app using React Native Firebase v26 messaging, why might Android 13 users never see a notification prompt, and how do you fix it?

level: seniorimportance: should knowfreq 28%

basics

~10 s

React Native Firebase's messaging requestPermission() is iOS-only: on Android it resolves AUTHORIZED without asking, and it is deprecated. Declare POST_NOTIFICATIONS in the app manifest and request it with PermissionsAndroid, or use expo-notifications.

open as a page

With expo-notifications on iOS, what does allowProvisional do, and why can checking only the root status field undo it?

level: seniorimportance: should knowfreq 25%

basics

~20 s

allowProvisional asks iOS for provisional authorization: no dialog, notifications delivered quietly to Notification Center. expo-notifications reports that as root status 'undetermined', so a generic granted-check fires the full prompt and throws the quiet trial away.

open as a page

Order-status pushes in a React Native pharmacy app silently stop reaching some users after weeks; how do you keep device tokens current on the backend?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Tokens rotate, so the app must re-send them: fetch the token on each launch and sign-in, subscribe to onTokenRefresh or addPushTokenListener, upsert one row per installation, detach on sign-out, and delete tokens the push service reports unregistered.

open as a page

With expo-notifications, how do you set or clear the app icon badge, and why can setBadgeCountAsync resolve false?

level: juniorimportance: nice to knowfreq 22%

basics

~10 s

Call Notifications.setBadgeCountAsync(n), where 0 clears the badge, and read it with getBadgeCountAsync. It resolves false on iOS when the badge permission was not granted, and on Android launchers that do not support icon badges.

open as a page

In a React Native app, why does a remote notification's image show on Android but not on iOS, and how do local notifications attach media?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

React Native Firebase's docs note Android shows an FCM notification image out of the box, while iOS needs a Notification Service Extension, a separate native target that fetches it. Local notifications attach media through expo-notifications' iOS-only content.attachments, using a file URL.

open as a page