With expo-notifications, how do you schedule a daily local medication reminder in a React Native app, and which trigger types exist?
answer
- local: scheduled on the device, no server
- scheduleNotificationAsync({ content, trigger })
- trigger type from SchedulableTriggerInputTypes
- returns an identifier to cancel
- weekday 1-7, Sunday is 1
basics
~10 sCall 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 sA 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 linesimport * 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
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.
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.
Treat the device schedule as state to reconcile: store identifiers per medication, cancel on change, avoid duplicates on launch and verify with getAllScheduledNotificationsAsync.
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