skip to content

Channels & Local Scheduling

Android channels fix importance and sound once created, and local notifications are scheduled on the device with triggers. Interviewers probe changing a channel and actionable categories.

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

explore

questions

6

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

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

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

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

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