Notifications
By the end of this lesson you will be able to show a notification from your app, schedule a daily reminder, react when the user taps a notification, and understand how push notifications travel from your server to a phone.
Notifications are powerful and easy to abuse. A train app that tells you your platform number is helpful. An app that sends "We miss you!" every evening gets uninstalled. Send fewer, more useful notifications.
Local vs push notifications
| Local notification | Push notification | |
|---|---|---|
| Who creates it | The app on the phone | Your server, through Apple's and Google's services |
| Needs internet | No | Yes |
| Example | "Drink water" every 2 hours | "Your order is out for delivery" |
| Works in Expo Go | Yes | No, needs a development build |
Think of a local notification as an alarm you set on your own clock. A push notification is a courier: someone else sends a parcel, and the postal system delivers it to your door.
npx expo install expo-notificationsStep 1: decide how notifications appear while the app is open
By default, a notification that arrives while your app is on screen is not shown. Set a handler once, near the top of your root layout, to choose what happens:
// src/app/_layout.jsx (outside the component)
import * as Notifications from "expo-notifications";
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowBanner: true, // show it at the top of the screen
shouldShowList: true, // keep it in the notification centre
shouldPlaySound: false,
shouldSetBadge: false,
}),
});You may see shouldShowAlert in older code. It is deprecated in favour of shouldShowBanner and shouldShowList.
Step 2: create an Android channel and ask permission
Android groups notifications into channels, such as "Order updates" and "Offers". Users can mute each channel separately in Settings. On Android 13 and newer, Expo's guide asks you to create at least one channel before you request permission or a push token, otherwise the request may not work as expected.
import { Platform } from "react-native";
import * as Notifications from "expo-notifications";
export async function setUpNotifications() {
if (Platform.OS === "android") {
await Notifications.setNotificationChannelAsync("reminders", {
name: "Reminders",
importance: Notifications.AndroidImportance.DEFAULT,
});
}
const { status } = await Notifications.requestPermissionsAsync();
return status === "granted";
}What you should see: on first call, the system asks "Allow Chai App to send you notifications?". The function returns true if the user allows it. On Android you can now find a "Reminders" category in the app's notification settings.
Step 3: schedule a local notification
scheduleNotificationAsync takes the content (title, body and extra data) and a trigger (when to show it). It returns an ID you can use to cancel it.
import * as Notifications from "expo-notifications";
export async function remindInFiveSeconds() {
return Notifications.scheduleNotificationAsync({
content: {
title: "Chai break",
body: "Time for a short walk and a cup of chai.",
data: { screen: "/breaks" },
},
trigger: {
type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL,
seconds: 5,
channelId: "reminders",
},
});
}
export async function remindDaily() {
return Notifications.scheduleNotificationAsync({
content: { title: "Revision time", body: "15 minutes of practice keeps the exam fear away." },
trigger: { type: Notifications.SchedulableTriggerInputTypes.DAILY, hour: 19, minute: 30, channelId: "reminders" },
});
}What you should see: five seconds after calling remindInFiveSeconds(), a banner slides down saying "Chai break". Even if you lock the phone, it still appears. remindDaily() shows "Revision time" every evening at 7:30 PM until you cancel it with Notifications.cancelScheduledNotificationAsync(id) or cancelAllScheduledNotificationsAsync(). A trigger of null shows the notification immediately.
Practise the logic: quiet hours
Good apps respect sleep. Before scheduling, check whether the time falls in the user's quiet hours. Times that cross midnight (22:00 to 07:00) are the tricky case. This is plain JavaScript, so run it here:
import { createRoot } from "react-dom/client";
function toMinutes(hhmm) {
const [h, m] = hhmm.split(":").map(Number);
return h * 60 + m;
}
function isQuiet(time, start, end) {
const t = toMinutes(time), s = toMinutes(start), e = toMinutes(end);
return s <= e ? t >= s && t < e : t >= s || t < e; // second case crosses midnight
}
function minutesUntil(from, to) {
return (toMinutes(to) - toMinutes(from) + 1440) % 1440;
}
const quiet = { start: "22:00", end: "07:00" };
["21:59", "22:00", "23:15", "03:40", "07:00", "13:30"].forEach((t) =>
console.log(t, isQuiet(t, quiet.start, quiet.end) ? "quiet" : "ok")
);
console.log("From 21:30 to 07:00 is", minutesUntil("21:30", "07:00"), "minutes");
createRoot(document.getElementById("root")).render(
<p>Quiet hours: {quiet.start} to {quiet.end}. Is 23:15 quiet? {String(isQuiet("23:15", quiet.start, quiet.end))}</p>
);21:59 ok 22:00 quiet 23:15 quiet 03:40 quiet 07:00 ok 13:30 ok From 21:30 to 07:00 is 570 minutes
If a reminder falls in quiet hours, you could move it to the end time instead.
Step 4: react when the user taps
When the user taps a notification, they expect to land on the right screen. Listen for responses and navigate using the data you attached.
// src/app/_layout.jsx
import { useEffect } from "react";
import { Stack, router } from "expo-router";
import * as Notifications from "expo-notifications";
export default function RootLayout() {
useEffect(() => {
const sub = Notifications.addNotificationResponseReceivedListener((response) => {
const screen = response.notification.request.content.data?.screen;
if (typeof screen === "string") router.push(screen);
});
return () => sub.remove();
}, []);
return <Stack />;
}What you should see: tapping the "Chai break" notification opens the app on the /breaks screen. For the case where the tap launched the app from a closed state, Expo also offers the useLastNotificationResponse() hook.
How push notifications work
Push needs a few more pieces, and a development build:
- The app asks for permission, then gets an Expo push token with
Notifications.getExpoPushTokenAsync({ projectId }). TheprojectIdcomes from your EAS project configuration. - The app sends this token to your server, which stores it with the user's account.
- When something happens (an order ships), your server sends the token, title and body to Expo's push service.
- Expo forwards it to Firebase Cloud Messaging (Android) or Apple Push Notification service (iOS), which deliver it to the phone.
Setting up the Android and iOS credentials is part of the EAS workflow covered in Build, Publish & Update with EAS. Never put the server-side push credentials inside the app.
Common mistakes
- Testing remote push in Expo Go. It is not supported; use a development build. Local notifications do work.
- Asking for permission before creating an Android channel. Create channels first.
- No handler. Without
setNotificationHandler, notifications that arrive while the app is open are not shown. - Old trigger shapes. Current triggers need a
type, such asSchedulableTriggerInputTypes.TIME_INTERVAL. - Never cancelling. Store the IDs of scheduled reminders so users can turn them off.
Key points
- Local notifications come from the app; push notifications come from your server.
setNotificationHandlerdecides how notifications appear while the app is open.- Create Android channels, then call
requestPermissionsAsync(). scheduleNotificationAsync({ content, trigger })schedules local notifications with typed triggers.- Use
addNotificationResponseReceivedListeneranddatato open the right screen on tap. - Push uses Expo push tokens, your server and a development build.
Exercise
Write a function scheduleMedicineReminders(times) that takes an array of "HH:MM" strings, for example ["08:00", "14:00", "21:00"], cancels all previously scheduled notifications, and schedules one daily reminder per time with the title "Medicine time" and the body "Take your tablet with water." Return the array of new notification IDs.
Show solution
import * as Notifications from "expo-notifications";
export async function scheduleMedicineReminders(times) {
await Notifications.cancelAllScheduledNotificationsAsync();
const ids = [];
for (const time of times) {
const [hour, minute] = time.split(":").map(Number);
const id = await Notifications.scheduleNotificationAsync({
content: { title: "Medicine time", body: "Take your tablet with water." },
trigger: { type: Notifications.SchedulableTriggerInputTypes.DAILY, hour, minute, channelId: "reminders" },
});
ids.push(id);
}
return ids;
}Cancelling first means changing the times never leaves old reminders behind. The returned IDs can be saved so single reminders can be cancelled later.
Quick Quiz
1. Which kind of notification works in Expo Go?
Expo Go supports local notifications; remote push needs a development build.
2. What does setNotificationHandler control?
The handler decides whether to show a banner, list entry, sound or badge while the app is open.
3. What must you do on Android before asking for notification permission?
On Android 13 and newer, Expo recommends creating a channel before requesting permission or a push token.
4. How do you open the right screen when a notification is tapped?
The response listener receives the tapped notification, including the data you attached.
Learn React Native with a mentor
Free lessons get you started. Our Mobile App Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
